changeset 0:bc5cc170163c

Initial revision
author Guido Berhoerster <guido+feed-preview@berhoerster.name>
date Wed, 03 Oct 2018 23:40:57 +0200
parents
children 1c31f4102408
files COPYING Makefile NEWS README _locales/de/messages.json _locales/en/messages.json background.js content_scripts/feed-preview.js content_scripts/feed-probe.js icons/feed-preview.svg manifest.json.in popup/feed-selection.html popup/feed-selection.js web_resources/feed-preview.xhtml web_resources/images/arrow.svg web_resources/style/common.css web_resources/style/entry-content.css web_resources/style/feed-preview.css web_resources/style/photon-colors.css
diffstat 19 files changed, 1705 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,78 @@
+#
+# Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+NAME =		feed-preview
+VERSION =	1
+EXT_NAME =	$(subst -,_,$(NAME))-$(VERSION)
+
+INKSCAPE := 	inkscape
+INFOZIP :=	zip
+SED :=		sed
+
+BITMAP_ICONS =	icons/feed-preview-48.png \
+		icons/feed-preview-96.png
+DIST_FILES =	manifest.json \
+		COPYING \
+		NEWS \
+		README \
+		$(wildcard _locales/*/messages.json) \
+		background.js \
+		content_scripts/feed-probe.js \
+		content_scripts/feed-preview.js \
+		icons/feed-preview.svg \
+		$(BITMAP_ICONS) \
+		popup/feed-selection.js \
+		popup/feed-selection.html \
+		web_resources/style/feed-preview.css \
+		web_resources/style/photon-colors.css \
+		web_resources/style/common.css \
+		web_resources/style/entry-content.css \
+		web_resources/feed-preview.xhtml \
+		web_resources/images/arrow.svg
+
+.DEFAULT_TARGET = all
+
+.PHONY: all extension clean clobber
+
+all: extension
+
+extension: $(EXT_NAME).zip
+
+$(EXT_NAME).zip: $(DIST_FILES)
+	$(INFOZIP) $@ $^
+
+define generate-icon-rule
+$1: $(1:%-$(lastword $(subst -, ,$1))=%.svg)
+	size=$(lastword $(subst -, ,$(basename $1))); \
+	    $(INKSCAPE) -w $$$${size} -h $$$${size} -e $$@ $$<
+endef
+
+$(foreach icon,$(BITMAP_ICONS),$(eval $(call generate-icon-rule,$(icon))))
+
+manifest.json: manifest.json.in
+	$(SED) 's|@VERSION@|$(VERSION)|g' $< >$@
+
+clean:
+	-rm -f $(BITMAP_ICONS) manifest.json
+
+clobber: clean
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NEWS	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,2 @@
+News
+====
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,40 @@
+Feed Preview
+============
+
+Feed Preview is a Firefox Addon which indicates the availability of RSS or
+Atom feeds in the browser's URL bar and renders a previews of feeds.
+
+Usage
+-----
+
+A feed icon in the browser's URL bar indicates whether there are RSS and/or
+Atom feeds available for the currently displayed page.  Click on the icon in
+order to open a menu containing the titles and types of available feeds.
+Select a feed from this menu in order to render a preview of that feed.
+Navigating to an Atom or RSS feed will also render a preview.
+
+Contact
+-------
+
+Please send any feedback, translations or bug reports via email to
+<guido+feed-preview@berhoerster.name>
+
+Bug Reports
+-----------
+
+When sending bug reports, please always mention the exact version of the addon
+with which the issue occurs as well as the version of Firefox and the operating
+system you are using and make sure that you provide sufficient information to
+reproduce the issue and include any error messages.
+
+License
+-------
+
+Except otherwise noted, all files are Copyright (C) 2018 Guido Berhoerster and
+distributed under the following license terms:
+
+Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_locales/de/messages.json	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,30 @@
+{
+    "extensionName": {
+        "message": "Feed Preview",
+        "description": "Name of the extension."
+    },
+    "extensionDescription": {
+        "message": "Signalisiert verfügbare RSS und/oder Atom Feeds und zeigt eine Vorschaui an.",
+        "description": "Description of the extension."
+    },
+    "defaultFeedTitle": {
+        "message": "Feed ohne Titel",
+        "description": "Default title for feeds."
+    },
+    "defaultFeedEntryTitle": {
+        "message": "Eintrag ohne Titel",
+        "description": "Default title for feed entries."
+    },
+    "defaultFileName": {
+        "message": "unbekannter-dateiname",
+        "description": "Default filename for media files."
+    },
+    "defaultFileType": {
+        "message": "unbekannter Dateityp",
+        "description": "Default media file type."
+    },
+    "filesTitle": {
+        "message": "Mediendateien:",
+        "description": "Title of the list of media files."
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_locales/en/messages.json	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,30 @@
+{
+    "extensionName": {
+        "message": "Feed Preview",
+        "description": "Name of the extension."
+    },
+    "extensionDescription": {
+        "message": "Indicates available RSS and Atom feeds and renders previews.",
+        "description": "Description of the extension."
+    },
+    "defaultFeedTitle": {
+        "message": "Untitled Feed",
+        "description": "Default title for feeds."
+    },
+    "defaultFeedEntryTitle": {
+        "message": "Untitled Entry",
+        "description": "Default title for feed entries."
+    },
+    "defaultFileName": {
+        "message": "unnamed-file",
+        "description": "Default filename for media files."
+    },
+    "defaultFileType": {
+        "message": "unknown type",
+        "description": "Default media file type."
+    },
+    "filesTitle": {
+        "message": "Media Files:",
+        "description": "Title of the list of media files."
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/background.js	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+var tabsFeeds = new Map();
+
+// until content handlers become available to webextensions
+// (https://bugzilla.mozilla.org/show_bug.cgi?id=1457500) intercept all
+// responses and change the content type from application/atom+xml or
+// application/rss+xml to application/xml which will then be handled by a
+// content script
+browser.webRequest.onHeadersReceived.addListener(details => {
+    if (details.statusCode != 200 ||
+            typeof details.responseHeaders === 'undefined') {
+        return;
+    }
+
+    let contentTypeHeader = details.responseHeaders.find(element => {
+        return element.name.toLowerCase() === 'content-type';
+    });
+    if (typeof contentTypeHeader !== 'undefined') {
+        let contentType = contentTypeHeader.value.split(';');
+        let mediaType = contentType[0].trim().toLowerCase();
+        if (mediaType === 'application/atom+xml' ||
+                mediaType === 'application/rss+xml') {
+            contentType[0] = 'application/xml';
+            contentTypeHeader.value = contentType.join(';');
+        }
+    }
+
+    return {responseHeaders: details.responseHeaders};
+}, {urls: ['http://*/*', 'https://*/*'], types: ['main_frame']},
+['blocking', 'responseHeaders']);
+
+browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
+    let tab = sender.tab;
+    if (typeof tab !== 'undefined') {
+        // content script sending feeds
+        tabsFeeds.set(tab.id, request);
+        browser.pageAction.show(tab.id);
+    } else {
+        let response = tabsFeeds.get(request);
+        // popup querying feeds
+        sendResponse(tabsFeeds.get(request));
+    }
+});
+
+browser.tabs.onUpdated.addListener((id, changeInfo, tab) => {
+    if (typeof changeInfo.url === 'undefined') {
+        // filter out updates which do not change the URL
+        return;
+    }
+
+    // hide the page action when the URL changes since it is no longer valid,
+    // it will be shown again if the content script detects a feed
+    browser.pageAction.hide(tab.id);
+});
+
+browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
+    tabsFeeds.delete(tabId);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content_scripts/feed-preview.js	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'ftp:']);
+
+function encodeXML(str) {
+    return str.replace(/[<>&'"]/g, c => {
+        switch (c) {
+            case '<': return '&lt;';
+            case '>': return '&gt;';
+            case '&': return '&amp;';
+            case '\'': return '&apos;';
+            case '"': return '&quot;';
+        }
+    });
+}
+
+function parseDate(s) {
+    let date = new Date(s);
+
+    return isNaN(date) ? new Date(0) : date;
+}
+
+function parseURL(text, baseURL = window.location.href) {
+    let url;
+
+    try {
+        url = new URL(text, baseURL);
+    } catch (e) {
+        return null;
+    }
+    if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
+        return null;
+    }
+
+    return url;
+}
+
+function normalizeHTML(text) {
+    let parsedDocument = (new DOMParser()).parseFromString(text, 'text/html');
+
+    let linkElement = parsedDocument.createElement('link');
+    linkElement.rel = 'stylesheet';
+    linkElement.href ='style/entry-content.css';
+    parsedDocument.head.appendChild(linkElement);
+
+    return (new XMLSerializer()).serializeToString(parsedDocument);
+}
+
+function nsMapper(prefix) {
+    switch (prefix) {
+        case 'atom':
+            return 'http://www.w3.org/2005/Atom'
+        case 'rss':
+            return 'http://my.netscape.com/rdf/simple/0.9/'
+    }
+    return null;
+}
+
+function xpathQuery(doc, scopeElement, xpathQuery, nsMapping) {
+    return doc.evaluate(xpathQuery, scopeElement, nsMapper,
+            XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+}
+
+function xpathQueryAll(doc, scopeElement, xpathQuery, nsMapping) {
+    let result = doc.evaluate(xpathQuery, scopeElement, nsMapper,
+            XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
+    let nodes = [];
+    for (let node = result.iterateNext(); node !== null;
+            node = result.iterateNext()) {
+            nodes.push(node);
+    }
+
+    return nodes;
+}
+
+class FeedLogo {
+    constructor(url, title = '') {
+        this.url = url;
+        this.title = title;
+    }
+}
+
+class RSS1Logo extends FeedLogo {
+    constructor(feedDocument, imageElement) {
+        let urlElement = xpathQuery(feedDocument, imageElement, './rss:url');
+        if (urlElement === null) {
+            throw new TypeError('missing <url> element in <logo> element');
+        }
+        let url = parseURL(urlElement.textContent.trim());
+        if (url === null) {
+            throw new TypeError('invalid URL in <logo> element');
+        }
+        super(url);
+
+        let titleElement = xpathQuery(feedDocument, imageElement,
+                './rss:title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent.trim();
+        }
+    }
+}
+
+class RSS2Logo extends FeedLogo {
+    constructor(feedDocument, imageElement) {
+        let urlElement = xpathQuery(feedDocument, imageElement, './url');
+        if (urlElement === null) {
+            throw new TypeError('missing <url> element in <logo> element');
+        }
+        let url = parseURL(urlElement.textContent.trim());
+        if (url === null) {
+            throw new TypeError('invalid URL in <logo> element');
+        }
+        super(url);
+
+        let titleElement = xpathQuery(feedDocument, imageElement, './title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent.trim();
+        }
+    }
+}
+
+class AtomLogo extends FeedLogo {
+    constructor(logoElement) {
+        let url = parseURL(logoElement.textContent.trim());
+        if (url === null) {
+            throw new TypeError('invalid URL in <logo> element');
+        }
+        super(url);
+    }
+}
+
+class FeedEntryFile {
+    constructor(url, type = browser.i18n.getMessage('defaultFileType'),
+            size = 0) {
+        this.url = url;
+        let filename = url.pathname.split('/').pop();
+        this.filename = filename !== '' ? filename :
+                browser.i18n.getMessage('defaultFileName');
+        this.type = type;
+        this.size = size;
+    }
+}
+
+class RSS2EntryFile extends FeedEntryFile {
+    constructor(enclosureElement) {
+        let url = parseURL(enclosureElement.getAttribute('url'));
+        if (url === null) {
+            throw new TypeError('invalid URL in <enclosure> element');
+        }
+        super(url);
+
+        let type = enclosureElement.getAttribute('type');
+        if (type !== null) {
+            this.type = type;
+        }
+
+        let size = parseInt(enclosureElement.getAttribute('length'), 10);
+        if (!isNaN(size)) {
+            this.size = size;
+        }
+    }
+}
+
+class FeedEntry {
+    constructor(title = browser.i18n.getMessage('defaultFeedEntryTitle'),
+            url = null, date = new Date(0), content = '', files = []) {
+        this.title = title;
+        this.url = url;
+        this.date = date;
+        this.content = content;
+        this.files = files;
+    }
+}
+
+class RSS1Entry extends FeedEntry {
+    constructor(feedDocument, itemElement) {
+        super();
+
+        let titleElement = xpathQuery(feedDocument, itemElement, './rss:title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent;
+        }
+
+        let linkElement = xpathQuery(feedDocument, itemElement, './rss:link');
+        if (linkElement !== null) {
+            this.url = parseURL(linkElement.textContent);
+        }
+    }
+}
+
+class RSS2Entry extends FeedEntry {
+    constructor(feedDocument, itemElement) {
+        super();
+
+        let titleElement = xpathQuery(feedDocument, itemElement, './title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent;
+        }
+
+        let linkElement = xpathQuery(feedDocument, itemElement, './link');
+        if (linkElement !== null) {
+            this.url = parseURL(linkElement.textContent);
+        }
+
+        let pubDateElement = xpathQuery(feedDocument, itemElement, './pubDate');
+        if (pubDateElement !== null) {
+            this.date = parseDate(pubDateElement.textContent);
+        }
+
+        let descriptionElement = xpathQuery(feedDocument, itemElement,
+                './description');
+        if (descriptionElement !== null) {
+            this.content = normalizeHTML(descriptionElement.textContent.trim());
+        }
+
+        for (let enclosureElement of xpathQueryAll(feedDocument, itemElement,
+                './enclosure')) {
+            try {
+                let entryFile = new RSS2EntryFile(enclosureElement);
+                this.files.push(entryFile);
+            } catch (e) {}
+        }
+    }
+}
+
+class AtomEntry extends FeedEntry {
+    constructor(feedDocument, entryElement) {
+        super();
+
+        let titleElement = xpathQuery(feedDocument, entryElement,
+                './atom:title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent.trim();
+        }
+
+        let linkElement = xpathQuery(feedDocument, entryElement,
+                './atom:link[@href][@rel="alternate"]');
+        if (linkElement !== null) {
+            this.url = parseURL(linkElement.getAttribute('href'));
+        }
+
+        let updatedElement = xpathQuery(feedDocument, entryElement,
+                './atom:updated');
+        if (updatedElement !== null) {
+            this.date = parseDate(updatedElement.textContent);
+        }
+
+        let contentElement = xpathQuery(feedDocument, entryElement,
+                './atom:content');
+        if (contentElement === null) {
+            contentElement = xpathQuery(feedDocument, entryElement,
+                    './atom:summary');
+        }
+        if (contentElement !== null) {
+            let contentType = contentElement.getAttribute('type');
+            if (contentType === null) {
+                contentType = 'text';
+            }
+            contentType = contentType.toLowerCase();
+            if (contentType === 'xhtml') {
+                this.content = normalizeHTML(contentElement.innerHTML);
+            } else if (contentType === 'html') {
+                this.content = normalizeHTML(contentElement.textContent);
+            } else {
+                let encodedContent =
+                        encodeXML(contentElement.textContent.trim());
+                this.content = normalizeHTML(`<pre>${encodedContent}</pre>`);
+            }
+        }
+    }
+}
+
+class Feed {
+    constructor(title = browser.i18n.getMessage('defaultFeedTitle'),
+            subtitle = '', logo = null, entries = []) {
+        this.title = title;
+        this.subtitle = subtitle;
+        this.logo = logo;
+        this.entries = entries;
+    }
+
+    async createPreviewDocument() {
+        let url = browser.extension.getURL('web_resources/feed-preview.xhtml');
+        let response;
+        let text;
+        try {
+            response = await fetch(url);
+            text = await response.text();
+        } catch (e) {
+            console.log(`Error: failed to read preview template: ${e.message}`);
+            return;
+        }
+        let previewDocument = (new DOMParser()).parseFromString(text,
+                'application/xhtml+xml');
+
+        previewDocument.querySelector('base').href =
+                browser.extension.getURL('web_resources/');
+
+        previewDocument.querySelector('title').textContent = this.title;
+        previewDocument.querySelector('#feed-title').textContent = this.title;
+        previewDocument.querySelector('#feed-subtitle').textContent =
+                this.subtitle;
+        if (this.logo !== null) {
+            let feedLogoTemplate =
+                    previewDocument.querySelector('#feed-logo-template');
+            let logoNode = previewDocument.importNode(feedLogoTemplate.content,
+                    true);
+            let imgElement = logoNode.querySelector('#feed-logo');
+            imgElement.setAttribute('src', this.logo.url);
+            imgElement.setAttribute('alt', this.logo.title);
+            previewDocument.querySelector('#feed-header').prepend(logoNode);
+        }
+
+        let entryTemplateElement =
+                previewDocument.querySelector('#entry-template');
+        let entryTitleTemplateElement =
+                previewDocument.querySelector('#entry-title-template');
+        let entryTitleLinkedTemplateElement =
+                previewDocument.querySelector('#entry-title-linked-template');
+        let entryFileListTemplateElement =
+                previewDocument.querySelector('#entry-files-list-template');
+        let entryFileTemplateElement =
+                previewDocument.querySelector('#entry-file-template');
+        for (let entry of this.entries) {
+            let entryNode =
+                    previewDocument.importNode(entryTemplateElement.content,
+                    true);
+            let titleElement;
+            let titleNode;
+
+            if (entry.url !== null) {
+                titleNode = previewDocument
+                        .importNode(entryTitleLinkedTemplateElement.content,
+                        true);
+                titleElement = titleNode.querySelector('.entry-link');
+                titleElement.href = entry.url;
+                titleElement.title = entry.title;
+            } else {
+                titleNode = previewDocument
+                        .importNode(entryTitleTemplateElement.content, true);
+                titleElement = titleNode.querySelector('.entry-title');
+            }
+            titleElement.textContent = entry.title;
+            entryNode.querySelector('.entry-header').prepend(titleNode);
+
+            let timeElement = entryNode.querySelector('.entry-date > time');
+            timeElement.textContent = entry.date.toLocaleString();
+
+            let contentElement = entryNode.querySelector('.entry-content');
+            contentElement.srcdoc = entry.content;
+            contentElement.title = entry.title;
+
+            if (entry.files.length > 0) {
+                let fileListNode = previewDocument
+                        .importNode(entryFileListTemplateElement.content, true);
+                fileListNode.querySelector('.entry-files-title').textContent =
+                        browser.i18n.getMessage('filesTitle');
+                let fileListElement =
+                        fileListNode.querySelector('.entry-files-list');
+
+                for (let file of entry.files) {
+                    let fileNode = previewDocument
+                            .importNode(entryFileTemplateElement.content, true);
+
+                    let fileLinkElement =
+                            fileNode.querySelector('.entry-file-link');
+                    fileLinkElement.href = file.url;
+                    fileLinkElement.title = file.filename;
+                    fileLinkElement.textContent = file.filename;
+
+                    fileNode.querySelector('.entry-file-info').textContent =
+                            `(${file.type}, ${file.size} bytes)`;
+
+                    fileListElement.appendChild(fileNode);
+                }
+
+                entryNode.querySelector('.entry').append(fileListNode);
+            }
+
+            previewDocument.body.append(entryNode);
+        }
+
+        return previewDocument;
+    }
+}
+
+class RSS1Feed extends Feed {
+    constructor(feedDocument) {
+        super();
+
+        let documentElement = feedDocument.documentElement;
+        let titleElement = xpathQuery(feedDocument, documentElement,
+                './rss:channel/rss:title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent;
+        }
+
+        let descriptionElement = xpathQuery(feedDocument, documentElement,
+                './channel/description');
+        if (descriptionElement !== null) {
+            this.subtitle = descriptionElement.textContent;
+        }
+
+        let imageElement = xpathQuery(feedDocument, documentElement,
+                './rss:image');
+        if (imageElement !== null) {
+            try {
+                let logo = new RSS1Logo(feedDocument, imageElement);
+                this.logo = logo;
+            } catch (e) {}
+        }
+
+        let itemElements = xpathQueryAll(feedDocument, documentElement,
+                './rss:item');
+        for (let itemElement of itemElements) {
+            let entry = new RSS1Entry(feedDocument, itemElement);
+            if (typeof entry !== 'undefined') {
+                this.entries.push(entry);
+            }
+        }
+    }
+}
+
+class RSS2Feed extends Feed {
+    constructor(feedDocument) {
+        super();
+
+        let documentElement = feedDocument.documentElement;
+        let titleElement = xpathQuery(feedDocument, documentElement,
+                './channel/title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent;
+        }
+
+        let descriptionElement = xpathQuery(feedDocument, documentElement,
+                './channel/description');
+        if (descriptionElement !== null) {
+            this.subtitle = descriptionElement.textContent;
+        }
+
+        let imageElement = xpathQuery(feedDocument, documentElement,
+                './channel/image');
+        if (imageElement !== null) {
+            try {
+                let logo = new RSS2Logo(feedDocument, imageElement);
+                this.logo = logo;
+            } catch (e) {}
+        }
+
+        let itemElements = xpathQueryAll(feedDocument, documentElement,
+                './channel/item');
+        for (let itemElement of itemElements) {
+            let entry = new RSS2Entry(feedDocument, itemElement);
+            if (typeof entry !== 'undefined') {
+                this.entries.push(entry);
+            }
+        }
+    }
+}
+
+class AtomFeed extends Feed {
+    constructor(feedDocument, atomVersion) {
+        super();
+
+        let documentElement = feedDocument.documentElement;
+        let titleElement = xpathQuery(feedDocument, documentElement,
+                './atom:title');
+        if (titleElement !== null) {
+            this.title = titleElement.textContent.trim();
+        }
+
+        let subtitleElement = xpathQuery(feedDocument, documentElement,
+                './atom:subtitle');
+        if (subtitleElement !== null) {
+            this.subtitle = subtitleElement.textContent.trim();
+        }
+
+        let logoElement =  xpathQuery(feedDocument, documentElement,
+                './atom:logo');
+        if (logoElement !== null) {
+            try {
+                let logo = new AtomLogo(logoElement);
+                this.logo = logo;
+            } catch (e) {}
+        }
+
+        let entryElements = xpathQueryAll(feedDocument, documentElement,
+                './atom:entry');
+        for (let entryElement of entryElements) {
+            this.entries.push(new AtomEntry(feedDocument, entryElement));
+        }
+    }
+}
+
+function probeFeedType(feedDocument) {
+    if (feedDocument.documentElement.nodeName === 'feed') {
+        let version = feedDocument.documentElement.getAttribute('version');
+        if (version === null) {
+            version = '1.0';
+        }
+        for (let attr of feedDocument.documentElement.attributes) {
+            if (attr.name === 'xmlns' &&
+                    attr.value === 'http://www.w3.org/2005/Atom') {
+                return ['atom', version];
+            }
+        }
+    } else if (feedDocument.documentElement.nodeName === 'rss') {
+        let version = feedDocument.documentElement.getAttribute('version');
+        if (version !== null) {
+            return ['rss', version];
+        }
+    } else if (feedDocument.documentElement.localName.toLowerCase() === 'rdf') {
+        for (let attr of feedDocument.documentElement.attributes) {
+            if (attr.name === 'xmlns' &&
+                    attr.value === 'http://my.netscape.com/rdf/simple/0.9/') {
+                return ['rss', '0.9'];
+            }
+        }
+    }
+
+    return [undefined, undefined];
+}
+
+async function replaceDocumentWithPreview(type, version) {
+    let feed;
+    switch (type) {
+        case 'rss':
+            switch (version) {
+                case '0.9':
+                case '1.0':
+                    feed = new RSS1Feed(document, version);
+                    break;
+                case '0.90':
+                case '0.91':
+                case '0.92':
+                case '0.93':
+                case '0.94':
+                case '2.0':
+                    feed = new RSS2Feed(document, version);
+                    break;
+                default:
+                    return;
+            }
+            break;
+        case 'atom':
+            feed = new AtomFeed(document, version);
+            break;
+        default:
+            return;
+    }
+
+    // replace original document with preview
+    let previewDocument = await feed.createPreviewDocument();
+    if (typeof previewDocument === 'undefined') {
+        return;
+    }
+    let documentElement = previewDocument.documentElement;
+    document.replaceChild(document.importNode(documentElement, true),
+            document.documentElement);
+}
+
+let [type, version] = probeFeedType(document);
+if (typeof type !== 'undefined') {
+    replaceDocumentWithPreview(type, version);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content_scripts/feed-probe.js	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+const TIMEOUT_MAX = 800; // ms
+
+function getFeeds() {
+    let urlsFeeds = new Map();
+    let elements = document.querySelectorAll(':-moz-any(link, a)[href]' +
+            '[rel=alternate]:-moz-any([type="application/atom+xml"], ' +
+            '[type="application/rss+xml"])');
+
+    for (let element of elements) {
+        if (!element.href.match(/^https?:\/\//)) {
+            continue;
+        }
+
+        urlsFeeds.set(element.href, {
+            href: element.href,
+            title: element.title || browser.i18n.getMessage('defaultFeedTitle'),
+            type: element.type
+        })
+    }
+
+    return Array.from(urlsFeeds.values());
+}
+
+function probeLinkedFeeds() {
+    if (document.documentElement.nodeName.toUpperCase() !== 'HTML') {
+        return;
+    }
+
+    let feeds = getFeeds();
+    if (feeds.length === 0) {
+        return;
+    }
+
+    // the listener on the background page might not be ready, keep trying to
+    // send the message with an exponential backoff until TIMEOUT_MAX is
+    // reached
+    let timeout = 0;
+    let sendFeeds = () => {
+        browser.runtime.sendMessage(feeds).catch(e => {
+            timeout = (timeout > 0) ? timeout * 2 : 100;
+            if (timeout > TIMEOUT_MAX) {
+                console.log(`Error: failed to message the background page: ` +
+                        ` ${e.message}`);
+                return;
+            }
+            setTimeout(sendFeeds, timeout);
+        });
+    };
+    setTimeout(sendFeeds, timeout);
+}
+
+probeLinkedFeeds();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/icons/feed-preview.svg	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,17 @@
+<!--
+Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+  <rect y="0" width="16" height="16" rx="3" ry="3" fill="#ff9400"/>
+  <g>
+    <circle cx="4" cy="12" r="2" fill="#ffffff"/>
+    <g fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round">
+      <path d="m3 7c4 0 6 2 6 6"/>
+      <path d="m3 3c6 0 10 4 10 10"/>
+    </g>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/manifest.json.in	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,52 @@
+{
+    "manifest_version": 2,
+    "name": "__MSG_extensionName__",
+    "version": "@VERSION@",
+    "description": "__MSG_extensionDescription__",
+    "author": "Guido Berhoerster",
+    "homepage_url": "https://code.guido-berhoerster.org/addons/firefox-addons/feed-preview/",
+    "default_locale": "en",
+    "applications": {
+        "gecko": {
+            "id": "feed-preview@code.guido-berhoerster.org",
+            "strict_min_version": "60.0"
+        }
+    },
+    "icons": {
+        "48": "icons/feed-preview-48.png",
+        "96": "icons/feed-preview-96.png"
+    },
+    "permissions": [
+        "tabs",
+        "http://*/*",
+        "https://*/*",
+        "webRequest",
+        "webRequestBlocking"
+    ],
+    "background": {
+        "scripts": [ "background.js" ]
+    },
+    "content_scripts": [
+        {
+            "matches": [ "http://*/*", "https://*/*", "file:///*" ],
+            "js": [
+                "content_scripts/feed-probe.js",
+                "content_scripts/feed-preview.js"
+            ]
+        }
+    ],
+    "web_accessible_resources": [
+        "web_resources/feed-preview.xhtml",
+        "web_resources/arrow.svg",
+        "web_resources/style/common.css",
+        "web_resources/style/entry-content.css",
+        "web_resources/style/feed-preview.css",
+        "web_resources/style/photon-colors.css"
+    ],
+    "page_action": {
+        "browser_style": true,
+        "default_icon": "icons/feed-preview.svg",
+        "default_title": "Feeds",
+        "default_popup": "popup/feed-selection.html"
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/popup/feed-selection.html	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+<!--
+   Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+
+   This Source Code Form is subject to the terms of the Mozilla Public
+   License, v. 2.0. If a copy of the MPL was not distributed with this
+   file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+    <script src="feed-selection.js" defer></script>
+  </head>
+  <body>
+  <template id="feed-item-template">
+    <div class="panel-list-item" data-href="">
+      <div class="icon"><img src="" alt=""></div>
+      <div class="text"></div>
+    </div>
+  </template>
+  <div class="panel-section panel-section-list">
+  </div>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/popup/feed-selection.js	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+async function buildFeedSelection() {
+    let tabs = await browser.tabs.query({active: true, currentWindow: true});
+    let feeds = await browser.runtime.sendMessage(tabs[0].id);
+
+    let feedListElement = document.querySelector('.panel-section-list');
+    feedListElement.addEventListener('click', ev => {
+        // find selected list item element and open the feed in a new tab
+        for (let element = ev.target; element !== ev.currentTarget;
+                element = element.parentElement) {
+            if (element.classList.contains('panel-list-item')) {
+                browser.tabs.create({url: element.dataset.href});
+                break;
+            }
+        }
+        ev.preventDefault();
+    });
+
+    let templateElement = document.querySelector('#feed-item-template');
+    for (let feed of feeds) {
+        let feedNode = document.importNode(templateElement.content, true);
+        feedNode.querySelector('.panel-list-item').dataset.href = feed.href;
+
+        let prefix = (feed.type === 'application/atom+xml') ? 'Atom Feed' :
+                'RSS Feed';
+
+        let imgNode = feedNode.querySelector('.icon > img');
+        imgNode.src = browser.runtime.getURL('icons/feed-preview.svg');
+        imgNode.alt = prefix;
+
+        feedNode.querySelector('.text').textContent =
+                `${prefix}: ${feed.title}`;
+        feedListElement.appendChild(feedNode);
+    }
+}
+
+buildFeedSelection();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/feed-preview.xhtml	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,55 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta charset="utf-8"/>
+<!--
+   Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+
+   This Source Code Form is subject to the terms of the Mozilla Public
+   License, v. 2.0. If a copy of the MPL was not distributed with this
+   file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+    <meta name="viewport" content="width=device-width, initial-scale=1"/>
+    <base href=""/>
+    <link rel="stylesheet" href="style/feed-preview.css"/>
+    <title></title>
+  </head>
+  <body>
+  <template id="feed-logo-template">
+    <img id="feed-logo" src="" alt=""/>
+  </template>
+  <template id="entry-template">
+    <article>
+      <details class="entry">
+        <summary>
+          <header class="entry-header">
+            <p class="entry-date"><time></time></p>
+          </header>
+        </summary>
+        <iframe class="entry-content" srcdoc="" title="" sandbox=""
+        width="800" height="360"/>
+      </details>
+    </article>
+  </template>
+  <template id="entry-title-template">
+    <h1 class="entry-title"></h1>
+  </template>
+  <template id="entry-title-linked-template">
+    <h1 class="entry-title"><a class="entry-link" href="" title=""></a></h1>
+  </template>
+  <template id="entry-files-list-template">
+    <footer class="entry-files">
+      <h2 class="entry-files-title"></h2>
+      <ul class="entry-files-list">
+      </ul>
+    </footer>
+  </template>
+  <template id="entry-file-template">
+    <li class="entry-file"><a class="entry-file-link" href="" title=""></a>
+    <span class="entry-file-info"></span></li>
+  </template>
+  <header id="feed-header">
+    <h1 id="feed-title"></h1>
+    <p id="feed-subtitle"></p>
+  </header>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/images/arrow.svg	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,10 @@
+<!--
+Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+  <path d="m4 2 6 6-6 6" fill="none" stroke="#0c0c0d" stroke-width="4"/>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/style/common.css	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,48 @@
+@import url("photon-colors.css");
+
+html,
+body {
+  box-sizing: border-box;
+  margin: 0;
+  font: -moz-desktop;
+  font-size: 15px;
+  line-height: 1.4em;
+  color: var(--grey-90);
+}
+
+h1, h2, h3, h4, h5, h6 {
+  line-height: 1.15em;
+}
+
+:link {
+  color: var(--blue-50);
+  text-decoration: none;
+}
+
+:visited {
+  color: var(--blue-50);
+  text-decoration: none;
+}
+
+:link:hover,
+:link:active,
+:visited:hover,
+:visited:active {
+  text-decoration: underline;
+}
+
+:link:hover {
+  color: var(--blue-60);
+}
+
+:link:active {
+  color: var(--blue-70);
+}
+
+:visited:hover {
+  color: var(--blue-60);
+}
+
+:visited:active {
+  color: var(--blue-70);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/style/entry-content.css	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,7 @@
+@import url("common.css");
+
+html,
+body {
+  background: var(--white-100);
+  padding: 4px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/style/feed-preview.css	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,100 @@
+@import url("common.css");
+
+html,
+body {
+  padding: 16px 32px;
+  background: var(--grey-10);
+}
+
+#feed-header {
+  max-width: 80ch;
+  margin: 0 auto;
+}
+
+#feed-logo {
+  float: right;
+  max-width: 188px;
+  max-height: 48px;
+  margin: 0 0 8px 8px;
+}
+
+#feed-title {
+  font-size: 1.777em;
+  margin: 0 0 4px 0;
+}
+
+#feed-subtitle {
+  color: var(--grey-50);
+  margin: 0 0 16px 0;
+}
+
+.entry {
+  clear: both;
+  margin: 16px auto;
+  padding: 16px;
+  max-width: 80ch;
+  background: var(--white-100);
+  border-radius: 4px;
+  box-shadow: 0 1px 2px var(--grey-90-a40);
+}
+
+details.entry > summary {
+  display: flex;
+  align-items: center;
+  list-style-type: none;
+  padding: 0 8px;
+}
+
+details.entry > summary:focus {
+  outline: none;
+}
+
+details.entry > summary::before {
+  content: url('../images/arrow.svg');
+  width: 16px;
+  height: 16px;
+  flex: 0 0 16px;
+}
+
+details.entry[open] > summary {
+  margin: 0 0 8px 0;
+}
+
+details.entry[open] > summary::before {
+  transform: rotate(90deg);
+}
+
+.entry-header {
+  margin: 0 0 0 8px;
+}
+
+.entry-title {
+  font-size: 1.333em;
+  margin: 0 0 4px 0;
+}
+
+.entry-date {
+  color: var(--grey-50);
+  margin: 0;
+}
+
+.entry-content {
+  width: 100%;
+  height: 24em;
+  border: 1px solid var(--grey-90-a10);
+}
+
+.entry-files {
+  padding: 0 8px;
+}
+
+.entry-files-title {
+  font-size: 1em;
+  margin: 0 0 4px 0;
+}
+
+.entry-files-list {
+  margin: 0;
+  padding: 0 0 0 32px;
+  color: var(--grey-50);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web_resources/style/photon-colors.css	Wed Oct 03 23:40:57 2018 +0200
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Photon Colors CSS Variables v3.2.0 */
+
+:root {
+  --magenta-50: #ff1ad9;
+  --magenta-60: #ed00b5;
+  --magenta-70: #b5007f;
+  --magenta-80: #7d004f;
+  --magenta-90: #440027;
+
+  --purple-30: #c069ff;
+  --purple-40: #ad3bff;
+  --purple-50: #9400ff;
+  --purple-60: #8000d7;
+  --purple-70: #6200a4;
+  --purple-80: #440071;
+  --purple-90: #25003e;
+
+  --blue-40: #45a1ff;
+  --blue-50: #0a84ff;
+  --blue-50-a30: rgba(10, 132, 255, 0.3);
+  --blue-60: #0060df;
+  --blue-70: #003eaa;
+  --blue-80: #002275;
+  --blue-90: #000f40;
+
+  --teal-50: #00feff;
+  --teal-60: #00c8d7;
+  --teal-70: #008ea4;
+  --teal-80: #005a71;
+  --teal-90: #002d3e;
+
+  --green-50: #30e60b;
+  --green-60: #12bc00;
+  --green-70: #058b00;
+  --green-80: #006504;
+  --green-90: #003706;
+
+  --yellow-50: #ffe900;
+  --yellow-60: #d7b600;
+  --yellow-70: #a47f00;
+  --yellow-80: #715100;
+  --yellow-90: #3e2800;
+
+  --red-50: #ff0039;
+  --red-60: #d70022;
+  --red-70: #a4000f;
+  --red-80: #5a0002;
+  --red-90: #3e0200;
+
+  --orange-50: #ff9400;
+  --orange-60: #d76e00;
+  --orange-70: #a44900;
+  --orange-80: #712b00;
+  --orange-90: #3e1300;
+
+  --grey-10: #f9f9fa;
+  --grey-10-a10: rgba(249, 249, 250, 0.1);
+  --grey-10-a20: rgba(249, 249, 250, 0.2);
+  --grey-10-a40: rgba(249, 249, 250, 0.4);
+  --grey-10-a60: rgba(249, 249, 250, 0.6);
+  --grey-10-a80: rgba(249, 249, 250, 0.8);
+  --grey-20: #ededf0;
+  --grey-30: #d7d7db;
+  --grey-40: #b1b1b3;
+  --grey-50: #737373;
+  --grey-60: #4a4a4f;
+  --grey-70: #38383d;
+  --grey-80: #2a2a2e;
+  --grey-90: #0c0c0d;
+  --grey-90-a05: rgba(12, 12, 13, 0.05);
+  --grey-90-a10: rgba(12, 12, 13, 0.1);
+  --grey-90-a20: rgba(12, 12, 13, 0.2);
+  --grey-90-a30: rgba(12, 12, 13, 0.3);
+  --grey-90-a40: rgba(12, 12, 13, 0.4);
+  --grey-90-a50: rgba(12, 12, 13, 0.5);
+  --grey-90-a60: rgba(12, 12, 13, 0.6);
+  --grey-90-a70: rgba(12, 12, 13, 0.7);
+  --grey-90-a80: rgba(12, 12, 13, 0.8);
+  --grey-90-a90: rgba(12, 12, 13, 0.9);
+
+  --ink-70: #363959;
+  --ink-80: #202340;
+  --ink-90: #0f1126;
+
+  --white-100: #ffffff;
+
+}