addons/firefox-addons/feed-preview

changeset 0:bc5cc170163c

Initial revision
author Guido Berhoerster <guido+feed-preview@berhoerster.name>
date Wed Oct 03 23:40:57 2018 +0200 (20 months ago)
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
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/COPYING	Wed Oct 03 23:40:57 2018 +0200
     1.3 @@ -0,0 +1,373 @@
     1.4 +Mozilla Public License Version 2.0
     1.5 +==================================
     1.6 +
     1.7 +1. Definitions
     1.8 +--------------
     1.9 +
    1.10 +1.1. "Contributor"
    1.11 +    means each individual or legal entity that creates, contributes to
    1.12 +    the creation of, or owns Covered Software.
    1.13 +
    1.14 +1.2. "Contributor Version"
    1.15 +    means the combination of the Contributions of others (if any) used
    1.16 +    by a Contributor and that particular Contributor's Contribution.
    1.17 +
    1.18 +1.3. "Contribution"
    1.19 +    means Covered Software of a particular Contributor.
    1.20 +
    1.21 +1.4. "Covered Software"
    1.22 +    means Source Code Form to which the initial Contributor has attached
    1.23 +    the notice in Exhibit A, the Executable Form of such Source Code
    1.24 +    Form, and Modifications of such Source Code Form, in each case
    1.25 +    including portions thereof.
    1.26 +
    1.27 +1.5. "Incompatible With Secondary Licenses"
    1.28 +    means
    1.29 +
    1.30 +    (a) that the initial Contributor has attached the notice described
    1.31 +        in Exhibit B to the Covered Software; or
    1.32 +
    1.33 +    (b) that the Covered Software was made available under the terms of
    1.34 +        version 1.1 or earlier of the License, but not also under the
    1.35 +        terms of a Secondary License.
    1.36 +
    1.37 +1.6. "Executable Form"
    1.38 +    means any form of the work other than Source Code Form.
    1.39 +
    1.40 +1.7. "Larger Work"
    1.41 +    means a work that combines Covered Software with other material, in 
    1.42 +    a separate file or files, that is not Covered Software.
    1.43 +
    1.44 +1.8. "License"
    1.45 +    means this document.
    1.46 +
    1.47 +1.9. "Licensable"
    1.48 +    means having the right to grant, to the maximum extent possible,
    1.49 +    whether at the time of the initial grant or subsequently, any and
    1.50 +    all of the rights conveyed by this License.
    1.51 +
    1.52 +1.10. "Modifications"
    1.53 +    means any of the following:
    1.54 +
    1.55 +    (a) any file in Source Code Form that results from an addition to,
    1.56 +        deletion from, or modification of the contents of Covered
    1.57 +        Software; or
    1.58 +
    1.59 +    (b) any new file in Source Code Form that contains any Covered
    1.60 +        Software.
    1.61 +
    1.62 +1.11. "Patent Claims" of a Contributor
    1.63 +    means any patent claim(s), including without limitation, method,
    1.64 +    process, and apparatus claims, in any patent Licensable by such
    1.65 +    Contributor that would be infringed, but for the grant of the
    1.66 +    License, by the making, using, selling, offering for sale, having
    1.67 +    made, import, or transfer of either its Contributions or its
    1.68 +    Contributor Version.
    1.69 +
    1.70 +1.12. "Secondary License"
    1.71 +    means either the GNU General Public License, Version 2.0, the GNU
    1.72 +    Lesser General Public License, Version 2.1, the GNU Affero General
    1.73 +    Public License, Version 3.0, or any later versions of those
    1.74 +    licenses.
    1.75 +
    1.76 +1.13. "Source Code Form"
    1.77 +    means the form of the work preferred for making modifications.
    1.78 +
    1.79 +1.14. "You" (or "Your")
    1.80 +    means an individual or a legal entity exercising rights under this
    1.81 +    License. For legal entities, "You" includes any entity that
    1.82 +    controls, is controlled by, or is under common control with You. For
    1.83 +    purposes of this definition, "control" means (a) the power, direct
    1.84 +    or indirect, to cause the direction or management of such entity,
    1.85 +    whether by contract or otherwise, or (b) ownership of more than
    1.86 +    fifty percent (50%) of the outstanding shares or beneficial
    1.87 +    ownership of such entity.
    1.88 +
    1.89 +2. License Grants and Conditions
    1.90 +--------------------------------
    1.91 +
    1.92 +2.1. Grants
    1.93 +
    1.94 +Each Contributor hereby grants You a world-wide, royalty-free,
    1.95 +non-exclusive license:
    1.96 +
    1.97 +(a) under intellectual property rights (other than patent or trademark)
    1.98 +    Licensable by such Contributor to use, reproduce, make available,
    1.99 +    modify, display, perform, distribute, and otherwise exploit its
   1.100 +    Contributions, either on an unmodified basis, with Modifications, or
   1.101 +    as part of a Larger Work; and
   1.102 +
   1.103 +(b) under Patent Claims of such Contributor to make, use, sell, offer
   1.104 +    for sale, have made, import, and otherwise transfer either its
   1.105 +    Contributions or its Contributor Version.
   1.106 +
   1.107 +2.2. Effective Date
   1.108 +
   1.109 +The licenses granted in Section 2.1 with respect to any Contribution
   1.110 +become effective for each Contribution on the date the Contributor first
   1.111 +distributes such Contribution.
   1.112 +
   1.113 +2.3. Limitations on Grant Scope
   1.114 +
   1.115 +The licenses granted in this Section 2 are the only rights granted under
   1.116 +this License. No additional rights or licenses will be implied from the
   1.117 +distribution or licensing of Covered Software under this License.
   1.118 +Notwithstanding Section 2.1(b) above, no patent license is granted by a
   1.119 +Contributor:
   1.120 +
   1.121 +(a) for any code that a Contributor has removed from Covered Software;
   1.122 +    or
   1.123 +
   1.124 +(b) for infringements caused by: (i) Your and any other third party's
   1.125 +    modifications of Covered Software, or (ii) the combination of its
   1.126 +    Contributions with other software (except as part of its Contributor
   1.127 +    Version); or
   1.128 +
   1.129 +(c) under Patent Claims infringed by Covered Software in the absence of
   1.130 +    its Contributions.
   1.131 +
   1.132 +This License does not grant any rights in the trademarks, service marks,
   1.133 +or logos of any Contributor (except as may be necessary to comply with
   1.134 +the notice requirements in Section 3.4).
   1.135 +
   1.136 +2.4. Subsequent Licenses
   1.137 +
   1.138 +No Contributor makes additional grants as a result of Your choice to
   1.139 +distribute the Covered Software under a subsequent version of this
   1.140 +License (see Section 10.2) or under the terms of a Secondary License (if
   1.141 +permitted under the terms of Section 3.3).
   1.142 +
   1.143 +2.5. Representation
   1.144 +
   1.145 +Each Contributor represents that the Contributor believes its
   1.146 +Contributions are its original creation(s) or it has sufficient rights
   1.147 +to grant the rights to its Contributions conveyed by this License.
   1.148 +
   1.149 +2.6. Fair Use
   1.150 +
   1.151 +This License is not intended to limit any rights You have under
   1.152 +applicable copyright doctrines of fair use, fair dealing, or other
   1.153 +equivalents.
   1.154 +
   1.155 +2.7. Conditions
   1.156 +
   1.157 +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
   1.158 +in Section 2.1.
   1.159 +
   1.160 +3. Responsibilities
   1.161 +-------------------
   1.162 +
   1.163 +3.1. Distribution of Source Form
   1.164 +
   1.165 +All distribution of Covered Software in Source Code Form, including any
   1.166 +Modifications that You create or to which You contribute, must be under
   1.167 +the terms of this License. You must inform recipients that the Source
   1.168 +Code Form of the Covered Software is governed by the terms of this
   1.169 +License, and how they can obtain a copy of this License. You may not
   1.170 +attempt to alter or restrict the recipients' rights in the Source Code
   1.171 +Form.
   1.172 +
   1.173 +3.2. Distribution of Executable Form
   1.174 +
   1.175 +If You distribute Covered Software in Executable Form then:
   1.176 +
   1.177 +(a) such Covered Software must also be made available in Source Code
   1.178 +    Form, as described in Section 3.1, and You must inform recipients of
   1.179 +    the Executable Form how they can obtain a copy of such Source Code
   1.180 +    Form by reasonable means in a timely manner, at a charge no more
   1.181 +    than the cost of distribution to the recipient; and
   1.182 +
   1.183 +(b) You may distribute such Executable Form under the terms of this
   1.184 +    License, or sublicense it under different terms, provided that the
   1.185 +    license for the Executable Form does not attempt to limit or alter
   1.186 +    the recipients' rights in the Source Code Form under this License.
   1.187 +
   1.188 +3.3. Distribution of a Larger Work
   1.189 +
   1.190 +You may create and distribute a Larger Work under terms of Your choice,
   1.191 +provided that You also comply with the requirements of this License for
   1.192 +the Covered Software. If the Larger Work is a combination of Covered
   1.193 +Software with a work governed by one or more Secondary Licenses, and the
   1.194 +Covered Software is not Incompatible With Secondary Licenses, this
   1.195 +License permits You to additionally distribute such Covered Software
   1.196 +under the terms of such Secondary License(s), so that the recipient of
   1.197 +the Larger Work may, at their option, further distribute the Covered
   1.198 +Software under the terms of either this License or such Secondary
   1.199 +License(s).
   1.200 +
   1.201 +3.4. Notices
   1.202 +
   1.203 +You may not remove or alter the substance of any license notices
   1.204 +(including copyright notices, patent notices, disclaimers of warranty,
   1.205 +or limitations of liability) contained within the Source Code Form of
   1.206 +the Covered Software, except that You may alter any license notices to
   1.207 +the extent required to remedy known factual inaccuracies.
   1.208 +
   1.209 +3.5. Application of Additional Terms
   1.210 +
   1.211 +You may choose to offer, and to charge a fee for, warranty, support,
   1.212 +indemnity or liability obligations to one or more recipients of Covered
   1.213 +Software. However, You may do so only on Your own behalf, and not on
   1.214 +behalf of any Contributor. You must make it absolutely clear that any
   1.215 +such warranty, support, indemnity, or liability obligation is offered by
   1.216 +You alone, and You hereby agree to indemnify every Contributor for any
   1.217 +liability incurred by such Contributor as a result of warranty, support,
   1.218 +indemnity or liability terms You offer. You may include additional
   1.219 +disclaimers of warranty and limitations of liability specific to any
   1.220 +jurisdiction.
   1.221 +
   1.222 +4. Inability to Comply Due to Statute or Regulation
   1.223 +---------------------------------------------------
   1.224 +
   1.225 +If it is impossible for You to comply with any of the terms of this
   1.226 +License with respect to some or all of the Covered Software due to
   1.227 +statute, judicial order, or regulation then You must: (a) comply with
   1.228 +the terms of this License to the maximum extent possible; and (b)
   1.229 +describe the limitations and the code they affect. Such description must
   1.230 +be placed in a text file included with all distributions of the Covered
   1.231 +Software under this License. Except to the extent prohibited by statute
   1.232 +or regulation, such description must be sufficiently detailed for a
   1.233 +recipient of ordinary skill to be able to understand it.
   1.234 +
   1.235 +5. Termination
   1.236 +--------------
   1.237 +
   1.238 +5.1. The rights granted under this License will terminate automatically
   1.239 +if You fail to comply with any of its terms. However, if You become
   1.240 +compliant, then the rights granted under this License from a particular
   1.241 +Contributor are reinstated (a) provisionally, unless and until such
   1.242 +Contributor explicitly and finally terminates Your grants, and (b) on an
   1.243 +ongoing basis, if such Contributor fails to notify You of the
   1.244 +non-compliance by some reasonable means prior to 60 days after You have
   1.245 +come back into compliance. Moreover, Your grants from a particular
   1.246 +Contributor are reinstated on an ongoing basis if such Contributor
   1.247 +notifies You of the non-compliance by some reasonable means, this is the
   1.248 +first time You have received notice of non-compliance with this License
   1.249 +from such Contributor, and You become compliant prior to 30 days after
   1.250 +Your receipt of the notice.
   1.251 +
   1.252 +5.2. If You initiate litigation against any entity by asserting a patent
   1.253 +infringement claim (excluding declaratory judgment actions,
   1.254 +counter-claims, and cross-claims) alleging that a Contributor Version
   1.255 +directly or indirectly infringes any patent, then the rights granted to
   1.256 +You by any and all Contributors for the Covered Software under Section
   1.257 +2.1 of this License shall terminate.
   1.258 +
   1.259 +5.3. In the event of termination under Sections 5.1 or 5.2 above, all
   1.260 +end user license agreements (excluding distributors and resellers) which
   1.261 +have been validly granted by You or Your distributors under this License
   1.262 +prior to termination shall survive termination.
   1.263 +
   1.264 +************************************************************************
   1.265 +*                                                                      *
   1.266 +*  6. Disclaimer of Warranty                                           *
   1.267 +*  -------------------------                                           *
   1.268 +*                                                                      *
   1.269 +*  Covered Software is provided under this License on an "as is"       *
   1.270 +*  basis, without warranty of any kind, either expressed, implied, or  *
   1.271 +*  statutory, including, without limitation, warranties that the       *
   1.272 +*  Covered Software is free of defects, merchantable, fit for a        *
   1.273 +*  particular purpose or non-infringing. The entire risk as to the     *
   1.274 +*  quality and performance of the Covered Software is with You.        *
   1.275 +*  Should any Covered Software prove defective in any respect, You     *
   1.276 +*  (not any Contributor) assume the cost of any necessary servicing,   *
   1.277 +*  repair, or correction. This disclaimer of warranty constitutes an   *
   1.278 +*  essential part of this License. No use of any Covered Software is   *
   1.279 +*  authorized under this License except under this disclaimer.         *
   1.280 +*                                                                      *
   1.281 +************************************************************************
   1.282 +
   1.283 +************************************************************************
   1.284 +*                                                                      *
   1.285 +*  7. Limitation of Liability                                          *
   1.286 +*  --------------------------                                          *
   1.287 +*                                                                      *
   1.288 +*  Under no circumstances and under no legal theory, whether tort      *
   1.289 +*  (including negligence), contract, or otherwise, shall any           *
   1.290 +*  Contributor, or anyone who distributes Covered Software as          *
   1.291 +*  permitted above, be liable to You for any direct, indirect,         *
   1.292 +*  special, incidental, or consequential damages of any character      *
   1.293 +*  including, without limitation, damages for lost profits, loss of    *
   1.294 +*  goodwill, work stoppage, computer failure or malfunction, or any    *
   1.295 +*  and all other commercial damages or losses, even if such party      *
   1.296 +*  shall have been informed of the possibility of such damages. This   *
   1.297 +*  limitation of liability shall not apply to liability for death or   *
   1.298 +*  personal injury resulting from such party's negligence to the       *
   1.299 +*  extent applicable law prohibits such limitation. Some               *
   1.300 +*  jurisdictions do not allow the exclusion or limitation of           *
   1.301 +*  incidental or consequential damages, so this exclusion and          *
   1.302 +*  limitation may not apply to You.                                    *
   1.303 +*                                                                      *
   1.304 +************************************************************************
   1.305 +
   1.306 +8. Litigation
   1.307 +-------------
   1.308 +
   1.309 +Any litigation relating to this License may be brought only in the
   1.310 +courts of a jurisdiction where the defendant maintains its principal
   1.311 +place of business and such litigation shall be governed by laws of that
   1.312 +jurisdiction, without reference to its conflict-of-law provisions.
   1.313 +Nothing in this Section shall prevent a party's ability to bring
   1.314 +cross-claims or counter-claims.
   1.315 +
   1.316 +9. Miscellaneous
   1.317 +----------------
   1.318 +
   1.319 +This License represents the complete agreement concerning the subject
   1.320 +matter hereof. If any provision of this License is held to be
   1.321 +unenforceable, such provision shall be reformed only to the extent
   1.322 +necessary to make it enforceable. Any law or regulation which provides
   1.323 +that the language of a contract shall be construed against the drafter
   1.324 +shall not be used to construe this License against a Contributor.
   1.325 +
   1.326 +10. Versions of the License
   1.327 +---------------------------
   1.328 +
   1.329 +10.1. New Versions
   1.330 +
   1.331 +Mozilla Foundation is the license steward. Except as provided in Section
   1.332 +10.3, no one other than the license steward has the right to modify or
   1.333 +publish new versions of this License. Each version will be given a
   1.334 +distinguishing version number.
   1.335 +
   1.336 +10.2. Effect of New Versions
   1.337 +
   1.338 +You may distribute the Covered Software under the terms of the version
   1.339 +of the License under which You originally received the Covered Software,
   1.340 +or under the terms of any subsequent version published by the license
   1.341 +steward.
   1.342 +
   1.343 +10.3. Modified Versions
   1.344 +
   1.345 +If you create software not governed by this License, and you want to
   1.346 +create a new license for such software, you may create and use a
   1.347 +modified version of this License if you rename the license and remove
   1.348 +any references to the name of the license steward (except to note that
   1.349 +such modified license differs from this License).
   1.350 +
   1.351 +10.4. Distributing Source Code Form that is Incompatible With Secondary
   1.352 +Licenses
   1.353 +
   1.354 +If You choose to distribute Source Code Form that is Incompatible With
   1.355 +Secondary Licenses under the terms of this version of the License, the
   1.356 +notice described in Exhibit B of this License must be attached.
   1.357 +
   1.358 +Exhibit A - Source Code Form License Notice
   1.359 +-------------------------------------------
   1.360 +
   1.361 +  This Source Code Form is subject to the terms of the Mozilla Public
   1.362 +  License, v. 2.0. If a copy of the MPL was not distributed with this
   1.363 +  file, You can obtain one at http://mozilla.org/MPL/2.0/.
   1.364 +
   1.365 +If it is not possible or desirable to put the notice in a particular
   1.366 +file, then You may include the notice in a location (such as a LICENSE
   1.367 +file in a relevant directory) where a recipient would be likely to look
   1.368 +for such a notice.
   1.369 +
   1.370 +You may add additional accurate notices of copyright ownership.
   1.371 +
   1.372 +Exhibit B - "Incompatible With Secondary Licenses" Notice
   1.373 +---------------------------------------------------------
   1.374 +
   1.375 +  This Source Code Form is "Incompatible With Secondary Licenses", as
   1.376 +  defined by the Mozilla Public License, v. 2.0.
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/Makefile	Wed Oct 03 23:40:57 2018 +0200
     2.3 @@ -0,0 +1,78 @@
     2.4 +#
     2.5 +# Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
     2.6 +#
     2.7 +# Permission is hereby granted, free of charge, to any person obtaining
     2.8 +# a copy of this software and associated documentation files (the
     2.9 +# "Software"), to deal in the Software without restriction, including
    2.10 +# without limitation the rights to use, copy, modify, merge, publish,
    2.11 +# distribute, sublicense, and/or sell copies of the Software, and to
    2.12 +# permit persons to whom the Software is furnished to do so, subject to
    2.13 +# the following conditions:
    2.14 +#
    2.15 +# The above copyright notice and this permission notice shall be included
    2.16 +# in all copies or substantial portions of the Software.
    2.17 +#
    2.18 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    2.19 +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    2.20 +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    2.21 +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    2.22 +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    2.23 +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    2.24 +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    2.25 +#
    2.26 +
    2.27 +NAME =		feed-preview
    2.28 +VERSION =	1
    2.29 +EXT_NAME =	$(subst -,_,$(NAME))-$(VERSION)
    2.30 +
    2.31 +INKSCAPE := 	inkscape
    2.32 +INFOZIP :=	zip
    2.33 +SED :=		sed
    2.34 +
    2.35 +BITMAP_ICONS =	icons/feed-preview-48.png \
    2.36 +		icons/feed-preview-96.png
    2.37 +DIST_FILES =	manifest.json \
    2.38 +		COPYING \
    2.39 +		NEWS \
    2.40 +		README \
    2.41 +		$(wildcard _locales/*/messages.json) \
    2.42 +		background.js \
    2.43 +		content_scripts/feed-probe.js \
    2.44 +		content_scripts/feed-preview.js \
    2.45 +		icons/feed-preview.svg \
    2.46 +		$(BITMAP_ICONS) \
    2.47 +		popup/feed-selection.js \
    2.48 +		popup/feed-selection.html \
    2.49 +		web_resources/style/feed-preview.css \
    2.50 +		web_resources/style/photon-colors.css \
    2.51 +		web_resources/style/common.css \
    2.52 +		web_resources/style/entry-content.css \
    2.53 +		web_resources/feed-preview.xhtml \
    2.54 +		web_resources/images/arrow.svg
    2.55 +
    2.56 +.DEFAULT_TARGET = all
    2.57 +
    2.58 +.PHONY: all extension clean clobber
    2.59 +
    2.60 +all: extension
    2.61 +
    2.62 +extension: $(EXT_NAME).zip
    2.63 +
    2.64 +$(EXT_NAME).zip: $(DIST_FILES)
    2.65 +	$(INFOZIP) $@ $^
    2.66 +
    2.67 +define generate-icon-rule
    2.68 +$1: $(1:%-$(lastword $(subst -, ,$1))=%.svg)
    2.69 +	size=$(lastword $(subst -, ,$(basename $1))); \
    2.70 +	    $(INKSCAPE) -w $$$${size} -h $$$${size} -e $$@ $$<
    2.71 +endef
    2.72 +
    2.73 +$(foreach icon,$(BITMAP_ICONS),$(eval $(call generate-icon-rule,$(icon))))
    2.74 +
    2.75 +manifest.json: manifest.json.in
    2.76 +	$(SED) 's|@VERSION@|$(VERSION)|g' $< >$@
    2.77 +
    2.78 +clean:
    2.79 +	-rm -f $(BITMAP_ICONS) manifest.json
    2.80 +
    2.81 +clobber: clean
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/NEWS	Wed Oct 03 23:40:57 2018 +0200
     3.3 @@ -0,0 +1,2 @@
     3.4 +News
     3.5 +====
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/README	Wed Oct 03 23:40:57 2018 +0200
     4.3 @@ -0,0 +1,40 @@
     4.4 +Feed Preview
     4.5 +============
     4.6 +
     4.7 +Feed Preview is a Firefox Addon which indicates the availability of RSS or
     4.8 +Atom feeds in the browser's URL bar and renders a previews of feeds.
     4.9 +
    4.10 +Usage
    4.11 +-----
    4.12 +
    4.13 +A feed icon in the browser's URL bar indicates whether there are RSS and/or
    4.14 +Atom feeds available for the currently displayed page.  Click on the icon in
    4.15 +order to open a menu containing the titles and types of available feeds.
    4.16 +Select a feed from this menu in order to render a preview of that feed.
    4.17 +Navigating to an Atom or RSS feed will also render a preview.
    4.18 +
    4.19 +Contact
    4.20 +-------
    4.21 +
    4.22 +Please send any feedback, translations or bug reports via email to
    4.23 +<guido+feed-preview@berhoerster.name>
    4.24 +
    4.25 +Bug Reports
    4.26 +-----------
    4.27 +
    4.28 +When sending bug reports, please always mention the exact version of the addon
    4.29 +with which the issue occurs as well as the version of Firefox and the operating
    4.30 +system you are using and make sure that you provide sufficient information to
    4.31 +reproduce the issue and include any error messages.
    4.32 +
    4.33 +License
    4.34 +-------
    4.35 +
    4.36 +Except otherwise noted, all files are Copyright (C) 2018 Guido Berhoerster and
    4.37 +distributed under the following license terms:
    4.38 +
    4.39 +Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
    4.40 +
    4.41 +This Source Code Form is subject to the terms of the Mozilla Public
    4.42 +License, v. 2.0. If a copy of the MPL was not distributed with this
    4.43 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/_locales/de/messages.json	Wed Oct 03 23:40:57 2018 +0200
     5.3 @@ -0,0 +1,30 @@
     5.4 +{
     5.5 +    "extensionName": {
     5.6 +        "message": "Feed Preview",
     5.7 +        "description": "Name of the extension."
     5.8 +    },
     5.9 +    "extensionDescription": {
    5.10 +        "message": "Signalisiert verf├╝gbare RSS und/oder Atom Feeds und zeigt eine Vorschaui an.",
    5.11 +        "description": "Description of the extension."
    5.12 +    },
    5.13 +    "defaultFeedTitle": {
    5.14 +        "message": "Feed ohne Titel",
    5.15 +        "description": "Default title for feeds."
    5.16 +    },
    5.17 +    "defaultFeedEntryTitle": {
    5.18 +        "message": "Eintrag ohne Titel",
    5.19 +        "description": "Default title for feed entries."
    5.20 +    },
    5.21 +    "defaultFileName": {
    5.22 +        "message": "unbekannter-dateiname",
    5.23 +        "description": "Default filename for media files."
    5.24 +    },
    5.25 +    "defaultFileType": {
    5.26 +        "message": "unbekannter Dateityp",
    5.27 +        "description": "Default media file type."
    5.28 +    },
    5.29 +    "filesTitle": {
    5.30 +        "message": "Mediendateien:",
    5.31 +        "description": "Title of the list of media files."
    5.32 +    }
    5.33 +}
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/_locales/en/messages.json	Wed Oct 03 23:40:57 2018 +0200
     6.3 @@ -0,0 +1,30 @@
     6.4 +{
     6.5 +    "extensionName": {
     6.6 +        "message": "Feed Preview",
     6.7 +        "description": "Name of the extension."
     6.8 +    },
     6.9 +    "extensionDescription": {
    6.10 +        "message": "Indicates available RSS and Atom feeds and renders previews.",
    6.11 +        "description": "Description of the extension."
    6.12 +    },
    6.13 +    "defaultFeedTitle": {
    6.14 +        "message": "Untitled Feed",
    6.15 +        "description": "Default title for feeds."
    6.16 +    },
    6.17 +    "defaultFeedEntryTitle": {
    6.18 +        "message": "Untitled Entry",
    6.19 +        "description": "Default title for feed entries."
    6.20 +    },
    6.21 +    "defaultFileName": {
    6.22 +        "message": "unnamed-file",
    6.23 +        "description": "Default filename for media files."
    6.24 +    },
    6.25 +    "defaultFileType": {
    6.26 +        "message": "unknown type",
    6.27 +        "description": "Default media file type."
    6.28 +    },
    6.29 +    "filesTitle": {
    6.30 +        "message": "Media Files:",
    6.31 +        "description": "Title of the list of media files."
    6.32 +    }
    6.33 +}
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/background.js	Wed Oct 03 23:40:57 2018 +0200
     7.3 @@ -0,0 +1,67 @@
     7.4 +/*
     7.5 + * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
     7.6 + *
     7.7 + * This Source Code Form is subject to the terms of the Mozilla Public
     7.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     7.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
    7.10 + */
    7.11 +
    7.12 +'use strict';
    7.13 +
    7.14 +var tabsFeeds = new Map();
    7.15 +
    7.16 +// until content handlers become available to webextensions
    7.17 +// (https://bugzilla.mozilla.org/show_bug.cgi?id=1457500) intercept all
    7.18 +// responses and change the content type from application/atom+xml or
    7.19 +// application/rss+xml to application/xml which will then be handled by a
    7.20 +// content script
    7.21 +browser.webRequest.onHeadersReceived.addListener(details => {
    7.22 +    if (details.statusCode != 200 ||
    7.23 +            typeof details.responseHeaders === 'undefined') {
    7.24 +        return;
    7.25 +    }
    7.26 +
    7.27 +    let contentTypeHeader = details.responseHeaders.find(element => {
    7.28 +        return element.name.toLowerCase() === 'content-type';
    7.29 +    });
    7.30 +    if (typeof contentTypeHeader !== 'undefined') {
    7.31 +        let contentType = contentTypeHeader.value.split(';');
    7.32 +        let mediaType = contentType[0].trim().toLowerCase();
    7.33 +        if (mediaType === 'application/atom+xml' ||
    7.34 +                mediaType === 'application/rss+xml') {
    7.35 +            contentType[0] = 'application/xml';
    7.36 +            contentTypeHeader.value = contentType.join(';');
    7.37 +        }
    7.38 +    }
    7.39 +
    7.40 +    return {responseHeaders: details.responseHeaders};
    7.41 +}, {urls: ['http://*/*', 'https://*/*'], types: ['main_frame']},
    7.42 +['blocking', 'responseHeaders']);
    7.43 +
    7.44 +browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
    7.45 +    let tab = sender.tab;
    7.46 +    if (typeof tab !== 'undefined') {
    7.47 +        // content script sending feeds
    7.48 +        tabsFeeds.set(tab.id, request);
    7.49 +        browser.pageAction.show(tab.id);
    7.50 +    } else {
    7.51 +        let response = tabsFeeds.get(request);
    7.52 +        // popup querying feeds
    7.53 +        sendResponse(tabsFeeds.get(request));
    7.54 +    }
    7.55 +});
    7.56 +
    7.57 +browser.tabs.onUpdated.addListener((id, changeInfo, tab) => {
    7.58 +    if (typeof changeInfo.url === 'undefined') {
    7.59 +        // filter out updates which do not change the URL
    7.60 +        return;
    7.61 +    }
    7.62 +
    7.63 +    // hide the page action when the URL changes since it is no longer valid,
    7.64 +    // it will be shown again if the content script detects a feed
    7.65 +    browser.pageAction.hide(tab.id);
    7.66 +});
    7.67 +
    7.68 +browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
    7.69 +    tabsFeeds.delete(tabId);
    7.70 +});
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/content_scripts/feed-preview.js	Wed Oct 03 23:40:57 2018 +0200
     8.3 @@ -0,0 +1,573 @@
     8.4 +/*
     8.5 + * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
     8.6 + *
     8.7 + * This Source Code Form is subject to the terms of the Mozilla Public
     8.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     8.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
    8.10 + */
    8.11 +
    8.12 +'use strict';
    8.13 +
    8.14 +const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'ftp:']);
    8.15 +
    8.16 +function encodeXML(str) {
    8.17 +    return str.replace(/[<>&'"]/g, c => {
    8.18 +        switch (c) {
    8.19 +            case '<': return '&lt;';
    8.20 +            case '>': return '&gt;';
    8.21 +            case '&': return '&amp;';
    8.22 +            case '\'': return '&apos;';
    8.23 +            case '"': return '&quot;';
    8.24 +        }
    8.25 +    });
    8.26 +}
    8.27 +
    8.28 +function parseDate(s) {
    8.29 +    let date = new Date(s);
    8.30 +
    8.31 +    return isNaN(date) ? new Date(0) : date;
    8.32 +}
    8.33 +
    8.34 +function parseURL(text, baseURL = window.location.href) {
    8.35 +    let url;
    8.36 +
    8.37 +    try {
    8.38 +        url = new URL(text, baseURL);
    8.39 +    } catch (e) {
    8.40 +        return null;
    8.41 +    }
    8.42 +    if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
    8.43 +        return null;
    8.44 +    }
    8.45 +
    8.46 +    return url;
    8.47 +}
    8.48 +
    8.49 +function normalizeHTML(text) {
    8.50 +    let parsedDocument = (new DOMParser()).parseFromString(text, 'text/html');
    8.51 +
    8.52 +    let linkElement = parsedDocument.createElement('link');
    8.53 +    linkElement.rel = 'stylesheet';
    8.54 +    linkElement.href ='style/entry-content.css';
    8.55 +    parsedDocument.head.appendChild(linkElement);
    8.56 +
    8.57 +    return (new XMLSerializer()).serializeToString(parsedDocument);
    8.58 +}
    8.59 +
    8.60 +function nsMapper(prefix) {
    8.61 +    switch (prefix) {
    8.62 +        case 'atom':
    8.63 +            return 'http://www.w3.org/2005/Atom'
    8.64 +        case 'rss':
    8.65 +            return 'http://my.netscape.com/rdf/simple/0.9/'
    8.66 +    }
    8.67 +    return null;
    8.68 +}
    8.69 +
    8.70 +function xpathQuery(doc, scopeElement, xpathQuery, nsMapping) {
    8.71 +    return doc.evaluate(xpathQuery, scopeElement, nsMapper,
    8.72 +            XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    8.73 +}
    8.74 +
    8.75 +function xpathQueryAll(doc, scopeElement, xpathQuery, nsMapping) {
    8.76 +    let result = doc.evaluate(xpathQuery, scopeElement, nsMapper,
    8.77 +            XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    8.78 +    let nodes = [];
    8.79 +    for (let node = result.iterateNext(); node !== null;
    8.80 +            node = result.iterateNext()) {
    8.81 +            nodes.push(node);
    8.82 +    }
    8.83 +
    8.84 +    return nodes;
    8.85 +}
    8.86 +
    8.87 +class FeedLogo {
    8.88 +    constructor(url, title = '') {
    8.89 +        this.url = url;
    8.90 +        this.title = title;
    8.91 +    }
    8.92 +}
    8.93 +
    8.94 +class RSS1Logo extends FeedLogo {
    8.95 +    constructor(feedDocument, imageElement) {
    8.96 +        let urlElement = xpathQuery(feedDocument, imageElement, './rss:url');
    8.97 +        if (urlElement === null) {
    8.98 +            throw new TypeError('missing <url> element in <logo> element');
    8.99 +        }
   8.100 +        let url = parseURL(urlElement.textContent.trim());
   8.101 +        if (url === null) {
   8.102 +            throw new TypeError('invalid URL in <logo> element');
   8.103 +        }
   8.104 +        super(url);
   8.105 +
   8.106 +        let titleElement = xpathQuery(feedDocument, imageElement,
   8.107 +                './rss:title');
   8.108 +        if (titleElement !== null) {
   8.109 +            this.title = titleElement.textContent.trim();
   8.110 +        }
   8.111 +    }
   8.112 +}
   8.113 +
   8.114 +class RSS2Logo extends FeedLogo {
   8.115 +    constructor(feedDocument, imageElement) {
   8.116 +        let urlElement = xpathQuery(feedDocument, imageElement, './url');
   8.117 +        if (urlElement === null) {
   8.118 +            throw new TypeError('missing <url> element in <logo> element');
   8.119 +        }
   8.120 +        let url = parseURL(urlElement.textContent.trim());
   8.121 +        if (url === null) {
   8.122 +            throw new TypeError('invalid URL in <logo> element');
   8.123 +        }
   8.124 +        super(url);
   8.125 +
   8.126 +        let titleElement = xpathQuery(feedDocument, imageElement, './title');
   8.127 +        if (titleElement !== null) {
   8.128 +            this.title = titleElement.textContent.trim();
   8.129 +        }
   8.130 +    }
   8.131 +}
   8.132 +
   8.133 +class AtomLogo extends FeedLogo {
   8.134 +    constructor(logoElement) {
   8.135 +        let url = parseURL(logoElement.textContent.trim());
   8.136 +        if (url === null) {
   8.137 +            throw new TypeError('invalid URL in <logo> element');
   8.138 +        }
   8.139 +        super(url);
   8.140 +    }
   8.141 +}
   8.142 +
   8.143 +class FeedEntryFile {
   8.144 +    constructor(url, type = browser.i18n.getMessage('defaultFileType'),
   8.145 +            size = 0) {
   8.146 +        this.url = url;
   8.147 +        let filename = url.pathname.split('/').pop();
   8.148 +        this.filename = filename !== '' ? filename :
   8.149 +                browser.i18n.getMessage('defaultFileName');
   8.150 +        this.type = type;
   8.151 +        this.size = size;
   8.152 +    }
   8.153 +}
   8.154 +
   8.155 +class RSS2EntryFile extends FeedEntryFile {
   8.156 +    constructor(enclosureElement) {
   8.157 +        let url = parseURL(enclosureElement.getAttribute('url'));
   8.158 +        if (url === null) {
   8.159 +            throw new TypeError('invalid URL in <enclosure> element');
   8.160 +        }
   8.161 +        super(url);
   8.162 +
   8.163 +        let type = enclosureElement.getAttribute('type');
   8.164 +        if (type !== null) {
   8.165 +            this.type = type;
   8.166 +        }
   8.167 +
   8.168 +        let size = parseInt(enclosureElement.getAttribute('length'), 10);
   8.169 +        if (!isNaN(size)) {
   8.170 +            this.size = size;
   8.171 +        }
   8.172 +    }
   8.173 +}
   8.174 +
   8.175 +class FeedEntry {
   8.176 +    constructor(title = browser.i18n.getMessage('defaultFeedEntryTitle'),
   8.177 +            url = null, date = new Date(0), content = '', files = []) {
   8.178 +        this.title = title;
   8.179 +        this.url = url;
   8.180 +        this.date = date;
   8.181 +        this.content = content;
   8.182 +        this.files = files;
   8.183 +    }
   8.184 +}
   8.185 +
   8.186 +class RSS1Entry extends FeedEntry {
   8.187 +    constructor(feedDocument, itemElement) {
   8.188 +        super();
   8.189 +
   8.190 +        let titleElement = xpathQuery(feedDocument, itemElement, './rss:title');
   8.191 +        if (titleElement !== null) {
   8.192 +            this.title = titleElement.textContent;
   8.193 +        }
   8.194 +
   8.195 +        let linkElement = xpathQuery(feedDocument, itemElement, './rss:link');
   8.196 +        if (linkElement !== null) {
   8.197 +            this.url = parseURL(linkElement.textContent);
   8.198 +        }
   8.199 +    }
   8.200 +}
   8.201 +
   8.202 +class RSS2Entry extends FeedEntry {
   8.203 +    constructor(feedDocument, itemElement) {
   8.204 +        super();
   8.205 +
   8.206 +        let titleElement = xpathQuery(feedDocument, itemElement, './title');
   8.207 +        if (titleElement !== null) {
   8.208 +            this.title = titleElement.textContent;
   8.209 +        }
   8.210 +
   8.211 +        let linkElement = xpathQuery(feedDocument, itemElement, './link');
   8.212 +        if (linkElement !== null) {
   8.213 +            this.url = parseURL(linkElement.textContent);
   8.214 +        }
   8.215 +
   8.216 +        let pubDateElement = xpathQuery(feedDocument, itemElement, './pubDate');
   8.217 +        if (pubDateElement !== null) {
   8.218 +            this.date = parseDate(pubDateElement.textContent);
   8.219 +        }
   8.220 +
   8.221 +        let descriptionElement = xpathQuery(feedDocument, itemElement,
   8.222 +                './description');
   8.223 +        if (descriptionElement !== null) {
   8.224 +            this.content = normalizeHTML(descriptionElement.textContent.trim());
   8.225 +        }
   8.226 +
   8.227 +        for (let enclosureElement of xpathQueryAll(feedDocument, itemElement,
   8.228 +                './enclosure')) {
   8.229 +            try {
   8.230 +                let entryFile = new RSS2EntryFile(enclosureElement);
   8.231 +                this.files.push(entryFile);
   8.232 +            } catch (e) {}
   8.233 +        }
   8.234 +    }
   8.235 +}
   8.236 +
   8.237 +class AtomEntry extends FeedEntry {
   8.238 +    constructor(feedDocument, entryElement) {
   8.239 +        super();
   8.240 +
   8.241 +        let titleElement = xpathQuery(feedDocument, entryElement,
   8.242 +                './atom:title');
   8.243 +        if (titleElement !== null) {
   8.244 +            this.title = titleElement.textContent.trim();
   8.245 +        }
   8.246 +
   8.247 +        let linkElement = xpathQuery(feedDocument, entryElement,
   8.248 +                './atom:link[@href][@rel="alternate"]');
   8.249 +        if (linkElement !== null) {
   8.250 +            this.url = parseURL(linkElement.getAttribute('href'));
   8.251 +        }
   8.252 +
   8.253 +        let updatedElement = xpathQuery(feedDocument, entryElement,
   8.254 +                './atom:updated');
   8.255 +        if (updatedElement !== null) {
   8.256 +            this.date = parseDate(updatedElement.textContent);
   8.257 +        }
   8.258 +
   8.259 +        let contentElement = xpathQuery(feedDocument, entryElement,
   8.260 +                './atom:content');
   8.261 +        if (contentElement === null) {
   8.262 +            contentElement = xpathQuery(feedDocument, entryElement,
   8.263 +                    './atom:summary');
   8.264 +        }
   8.265 +        if (contentElement !== null) {
   8.266 +            let contentType = contentElement.getAttribute('type');
   8.267 +            if (contentType === null) {
   8.268 +                contentType = 'text';
   8.269 +            }
   8.270 +            contentType = contentType.toLowerCase();
   8.271 +            if (contentType === 'xhtml') {
   8.272 +                this.content = normalizeHTML(contentElement.innerHTML);
   8.273 +            } else if (contentType === 'html') {
   8.274 +                this.content = normalizeHTML(contentElement.textContent);
   8.275 +            } else {
   8.276 +                let encodedContent =
   8.277 +                        encodeXML(contentElement.textContent.trim());
   8.278 +                this.content = normalizeHTML(`<pre>${encodedContent}</pre>`);
   8.279 +            }
   8.280 +        }
   8.281 +    }
   8.282 +}
   8.283 +
   8.284 +class Feed {
   8.285 +    constructor(title = browser.i18n.getMessage('defaultFeedTitle'),
   8.286 +            subtitle = '', logo = null, entries = []) {
   8.287 +        this.title = title;
   8.288 +        this.subtitle = subtitle;
   8.289 +        this.logo = logo;
   8.290 +        this.entries = entries;
   8.291 +    }
   8.292 +
   8.293 +    async createPreviewDocument() {
   8.294 +        let url = browser.extension.getURL('web_resources/feed-preview.xhtml');
   8.295 +        let response;
   8.296 +        let text;
   8.297 +        try {
   8.298 +            response = await fetch(url);
   8.299 +            text = await response.text();
   8.300 +        } catch (e) {
   8.301 +            console.log(`Error: failed to read preview template: ${e.message}`);
   8.302 +            return;
   8.303 +        }
   8.304 +        let previewDocument = (new DOMParser()).parseFromString(text,
   8.305 +                'application/xhtml+xml');
   8.306 +
   8.307 +        previewDocument.querySelector('base').href =
   8.308 +                browser.extension.getURL('web_resources/');
   8.309 +
   8.310 +        previewDocument.querySelector('title').textContent = this.title;
   8.311 +        previewDocument.querySelector('#feed-title').textContent = this.title;
   8.312 +        previewDocument.querySelector('#feed-subtitle').textContent =
   8.313 +                this.subtitle;
   8.314 +        if (this.logo !== null) {
   8.315 +            let feedLogoTemplate =
   8.316 +                    previewDocument.querySelector('#feed-logo-template');
   8.317 +            let logoNode = previewDocument.importNode(feedLogoTemplate.content,
   8.318 +                    true);
   8.319 +            let imgElement = logoNode.querySelector('#feed-logo');
   8.320 +            imgElement.setAttribute('src', this.logo.url);
   8.321 +            imgElement.setAttribute('alt', this.logo.title);
   8.322 +            previewDocument.querySelector('#feed-header').prepend(logoNode);
   8.323 +        }
   8.324 +
   8.325 +        let entryTemplateElement =
   8.326 +                previewDocument.querySelector('#entry-template');
   8.327 +        let entryTitleTemplateElement =
   8.328 +                previewDocument.querySelector('#entry-title-template');
   8.329 +        let entryTitleLinkedTemplateElement =
   8.330 +                previewDocument.querySelector('#entry-title-linked-template');
   8.331 +        let entryFileListTemplateElement =
   8.332 +                previewDocument.querySelector('#entry-files-list-template');
   8.333 +        let entryFileTemplateElement =
   8.334 +                previewDocument.querySelector('#entry-file-template');
   8.335 +        for (let entry of this.entries) {
   8.336 +            let entryNode =
   8.337 +                    previewDocument.importNode(entryTemplateElement.content,
   8.338 +                    true);
   8.339 +            let titleElement;
   8.340 +            let titleNode;
   8.341 +
   8.342 +            if (entry.url !== null) {
   8.343 +                titleNode = previewDocument
   8.344 +                        .importNode(entryTitleLinkedTemplateElement.content,
   8.345 +                        true);
   8.346 +                titleElement = titleNode.querySelector('.entry-link');
   8.347 +                titleElement.href = entry.url;
   8.348 +                titleElement.title = entry.title;
   8.349 +            } else {
   8.350 +                titleNode = previewDocument
   8.351 +                        .importNode(entryTitleTemplateElement.content, true);
   8.352 +                titleElement = titleNode.querySelector('.entry-title');
   8.353 +            }
   8.354 +            titleElement.textContent = entry.title;
   8.355 +            entryNode.querySelector('.entry-header').prepend(titleNode);
   8.356 +
   8.357 +            let timeElement = entryNode.querySelector('.entry-date > time');
   8.358 +            timeElement.textContent = entry.date.toLocaleString();
   8.359 +
   8.360 +            let contentElement = entryNode.querySelector('.entry-content');
   8.361 +            contentElement.srcdoc = entry.content;
   8.362 +            contentElement.title = entry.title;
   8.363 +
   8.364 +            if (entry.files.length > 0) {
   8.365 +                let fileListNode = previewDocument
   8.366 +                        .importNode(entryFileListTemplateElement.content, true);
   8.367 +                fileListNode.querySelector('.entry-files-title').textContent =
   8.368 +                        browser.i18n.getMessage('filesTitle');
   8.369 +                let fileListElement =
   8.370 +                        fileListNode.querySelector('.entry-files-list');
   8.371 +
   8.372 +                for (let file of entry.files) {
   8.373 +                    let fileNode = previewDocument
   8.374 +                            .importNode(entryFileTemplateElement.content, true);
   8.375 +
   8.376 +                    let fileLinkElement =
   8.377 +                            fileNode.querySelector('.entry-file-link');
   8.378 +                    fileLinkElement.href = file.url;
   8.379 +                    fileLinkElement.title = file.filename;
   8.380 +                    fileLinkElement.textContent = file.filename;
   8.381 +
   8.382 +                    fileNode.querySelector('.entry-file-info').textContent =
   8.383 +                            `(${file.type}, ${file.size} bytes)`;
   8.384 +
   8.385 +                    fileListElement.appendChild(fileNode);
   8.386 +                }
   8.387 +
   8.388 +                entryNode.querySelector('.entry').append(fileListNode);
   8.389 +            }
   8.390 +
   8.391 +            previewDocument.body.append(entryNode);
   8.392 +        }
   8.393 +
   8.394 +        return previewDocument;
   8.395 +    }
   8.396 +}
   8.397 +
   8.398 +class RSS1Feed extends Feed {
   8.399 +    constructor(feedDocument) {
   8.400 +        super();
   8.401 +
   8.402 +        let documentElement = feedDocument.documentElement;
   8.403 +        let titleElement = xpathQuery(feedDocument, documentElement,
   8.404 +                './rss:channel/rss:title');
   8.405 +        if (titleElement !== null) {
   8.406 +            this.title = titleElement.textContent;
   8.407 +        }
   8.408 +
   8.409 +        let descriptionElement = xpathQuery(feedDocument, documentElement,
   8.410 +                './channel/description');
   8.411 +        if (descriptionElement !== null) {
   8.412 +            this.subtitle = descriptionElement.textContent;
   8.413 +        }
   8.414 +
   8.415 +        let imageElement = xpathQuery(feedDocument, documentElement,
   8.416 +                './rss:image');
   8.417 +        if (imageElement !== null) {
   8.418 +            try {
   8.419 +                let logo = new RSS1Logo(feedDocument, imageElement);
   8.420 +                this.logo = logo;
   8.421 +            } catch (e) {}
   8.422 +        }
   8.423 +
   8.424 +        let itemElements = xpathQueryAll(feedDocument, documentElement,
   8.425 +                './rss:item');
   8.426 +        for (let itemElement of itemElements) {
   8.427 +            let entry = new RSS1Entry(feedDocument, itemElement);
   8.428 +            if (typeof entry !== 'undefined') {
   8.429 +                this.entries.push(entry);
   8.430 +            }
   8.431 +        }
   8.432 +    }
   8.433 +}
   8.434 +
   8.435 +class RSS2Feed extends Feed {
   8.436 +    constructor(feedDocument) {
   8.437 +        super();
   8.438 +
   8.439 +        let documentElement = feedDocument.documentElement;
   8.440 +        let titleElement = xpathQuery(feedDocument, documentElement,
   8.441 +                './channel/title');
   8.442 +        if (titleElement !== null) {
   8.443 +            this.title = titleElement.textContent;
   8.444 +        }
   8.445 +
   8.446 +        let descriptionElement = xpathQuery(feedDocument, documentElement,
   8.447 +                './channel/description');
   8.448 +        if (descriptionElement !== null) {
   8.449 +            this.subtitle = descriptionElement.textContent;
   8.450 +        }
   8.451 +
   8.452 +        let imageElement = xpathQuery(feedDocument, documentElement,
   8.453 +                './channel/image');
   8.454 +        if (imageElement !== null) {
   8.455 +            try {
   8.456 +                let logo = new RSS2Logo(feedDocument, imageElement);
   8.457 +                this.logo = logo;
   8.458 +            } catch (e) {}
   8.459 +        }
   8.460 +
   8.461 +        let itemElements = xpathQueryAll(feedDocument, documentElement,
   8.462 +                './channel/item');
   8.463 +        for (let itemElement of itemElements) {
   8.464 +            let entry = new RSS2Entry(feedDocument, itemElement);
   8.465 +            if (typeof entry !== 'undefined') {
   8.466 +                this.entries.push(entry);
   8.467 +            }
   8.468 +        }
   8.469 +    }
   8.470 +}
   8.471 +
   8.472 +class AtomFeed extends Feed {
   8.473 +    constructor(feedDocument, atomVersion) {
   8.474 +        super();
   8.475 +
   8.476 +        let documentElement = feedDocument.documentElement;
   8.477 +        let titleElement = xpathQuery(feedDocument, documentElement,
   8.478 +                './atom:title');
   8.479 +        if (titleElement !== null) {
   8.480 +            this.title = titleElement.textContent.trim();
   8.481 +        }
   8.482 +
   8.483 +        let subtitleElement = xpathQuery(feedDocument, documentElement,
   8.484 +                './atom:subtitle');
   8.485 +        if (subtitleElement !== null) {
   8.486 +            this.subtitle = subtitleElement.textContent.trim();
   8.487 +        }
   8.488 +
   8.489 +        let logoElement =  xpathQuery(feedDocument, documentElement,
   8.490 +                './atom:logo');
   8.491 +        if (logoElement !== null) {
   8.492 +            try {
   8.493 +                let logo = new AtomLogo(logoElement);
   8.494 +                this.logo = logo;
   8.495 +            } catch (e) {}
   8.496 +        }
   8.497 +
   8.498 +        let entryElements = xpathQueryAll(feedDocument, documentElement,
   8.499 +                './atom:entry');
   8.500 +        for (let entryElement of entryElements) {
   8.501 +            this.entries.push(new AtomEntry(feedDocument, entryElement));
   8.502 +        }
   8.503 +    }
   8.504 +}
   8.505 +
   8.506 +function probeFeedType(feedDocument) {
   8.507 +    if (feedDocument.documentElement.nodeName === 'feed') {
   8.508 +        let version = feedDocument.documentElement.getAttribute('version');
   8.509 +        if (version === null) {
   8.510 +            version = '1.0';
   8.511 +        }
   8.512 +        for (let attr of feedDocument.documentElement.attributes) {
   8.513 +            if (attr.name === 'xmlns' &&
   8.514 +                    attr.value === 'http://www.w3.org/2005/Atom') {
   8.515 +                return ['atom', version];
   8.516 +            }
   8.517 +        }
   8.518 +    } else if (feedDocument.documentElement.nodeName === 'rss') {
   8.519 +        let version = feedDocument.documentElement.getAttribute('version');
   8.520 +        if (version !== null) {
   8.521 +            return ['rss', version];
   8.522 +        }
   8.523 +    } else if (feedDocument.documentElement.localName.toLowerCase() === 'rdf') {
   8.524 +        for (let attr of feedDocument.documentElement.attributes) {
   8.525 +            if (attr.name === 'xmlns' &&
   8.526 +                    attr.value === 'http://my.netscape.com/rdf/simple/0.9/') {
   8.527 +                return ['rss', '0.9'];
   8.528 +            }
   8.529 +        }
   8.530 +    }
   8.531 +
   8.532 +    return [undefined, undefined];
   8.533 +}
   8.534 +
   8.535 +async function replaceDocumentWithPreview(type, version) {
   8.536 +    let feed;
   8.537 +    switch (type) {
   8.538 +        case 'rss':
   8.539 +            switch (version) {
   8.540 +                case '0.9':
   8.541 +                case '1.0':
   8.542 +                    feed = new RSS1Feed(document, version);
   8.543 +                    break;
   8.544 +                case '0.90':
   8.545 +                case '0.91':
   8.546 +                case '0.92':
   8.547 +                case '0.93':
   8.548 +                case '0.94':
   8.549 +                case '2.0':
   8.550 +                    feed = new RSS2Feed(document, version);
   8.551 +                    break;
   8.552 +                default:
   8.553 +                    return;
   8.554 +            }
   8.555 +            break;
   8.556 +        case 'atom':
   8.557 +            feed = new AtomFeed(document, version);
   8.558 +            break;
   8.559 +        default:
   8.560 +            return;
   8.561 +    }
   8.562 +
   8.563 +    // replace original document with preview
   8.564 +    let previewDocument = await feed.createPreviewDocument();
   8.565 +    if (typeof previewDocument === 'undefined') {
   8.566 +        return;
   8.567 +    }
   8.568 +    let documentElement = previewDocument.documentElement;
   8.569 +    document.replaceChild(document.importNode(documentElement, true),
   8.570 +            document.documentElement);
   8.571 +}
   8.572 +
   8.573 +let [type, version] = probeFeedType(document);
   8.574 +if (typeof type !== 'undefined') {
   8.575 +    replaceDocumentWithPreview(type, version);
   8.576 +}
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/content_scripts/feed-probe.js	Wed Oct 03 23:40:57 2018 +0200
     9.3 @@ -0,0 +1,62 @@
     9.4 +/*
     9.5 + * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
     9.6 + *
     9.7 + * This Source Code Form is subject to the terms of the Mozilla Public
     9.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     9.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
    9.10 + */
    9.11 +
    9.12 +'use strict';
    9.13 +
    9.14 +const TIMEOUT_MAX = 800; // ms
    9.15 +
    9.16 +function getFeeds() {
    9.17 +    let urlsFeeds = new Map();
    9.18 +    let elements = document.querySelectorAll(':-moz-any(link, a)[href]' +
    9.19 +            '[rel=alternate]:-moz-any([type="application/atom+xml"], ' +
    9.20 +            '[type="application/rss+xml"])');
    9.21 +
    9.22 +    for (let element of elements) {
    9.23 +        if (!element.href.match(/^https?:\/\//)) {
    9.24 +            continue;
    9.25 +        }
    9.26 +
    9.27 +        urlsFeeds.set(element.href, {
    9.28 +            href: element.href,
    9.29 +            title: element.title || browser.i18n.getMessage('defaultFeedTitle'),
    9.30 +            type: element.type
    9.31 +        })
    9.32 +    }
    9.33 +
    9.34 +    return Array.from(urlsFeeds.values());
    9.35 +}
    9.36 +
    9.37 +function probeLinkedFeeds() {
    9.38 +    if (document.documentElement.nodeName.toUpperCase() !== 'HTML') {
    9.39 +        return;
    9.40 +    }
    9.41 +
    9.42 +    let feeds = getFeeds();
    9.43 +    if (feeds.length === 0) {
    9.44 +        return;
    9.45 +    }
    9.46 +
    9.47 +    // the listener on the background page might not be ready, keep trying to
    9.48 +    // send the message with an exponential backoff until TIMEOUT_MAX is
    9.49 +    // reached
    9.50 +    let timeout = 0;
    9.51 +    let sendFeeds = () => {
    9.52 +        browser.runtime.sendMessage(feeds).catch(e => {
    9.53 +            timeout = (timeout > 0) ? timeout * 2 : 100;
    9.54 +            if (timeout > TIMEOUT_MAX) {
    9.55 +                console.log(`Error: failed to message the background page: ` +
    9.56 +                        ` ${e.message}`);
    9.57 +                return;
    9.58 +            }
    9.59 +            setTimeout(sendFeeds, timeout);
    9.60 +        });
    9.61 +    };
    9.62 +    setTimeout(sendFeeds, timeout);
    9.63 +}
    9.64 +
    9.65 +probeLinkedFeeds();
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/icons/feed-preview.svg	Wed Oct 03 23:40:57 2018 +0200
    10.3 @@ -0,0 +1,17 @@
    10.4 +<!--
    10.5 +Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
    10.6 +
    10.7 +This Source Code Form is subject to the terms of the Mozilla Public
    10.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    10.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   10.10 +-->
   10.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
   10.12 +  <rect y="0" width="16" height="16" rx="3" ry="3" fill="#ff9400"/>
   10.13 +  <g>
   10.14 +    <circle cx="4" cy="12" r="2" fill="#ffffff"/>
   10.15 +    <g fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round">
   10.16 +      <path d="m3 7c4 0 6 2 6 6"/>
   10.17 +      <path d="m3 3c6 0 10 4 10 10"/>
   10.18 +    </g>
   10.19 +  </g>
   10.20 +</svg>
    11.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.2 +++ b/manifest.json.in	Wed Oct 03 23:40:57 2018 +0200
    11.3 @@ -0,0 +1,52 @@
    11.4 +{
    11.5 +    "manifest_version": 2,
    11.6 +    "name": "__MSG_extensionName__",
    11.7 +    "version": "@VERSION@",
    11.8 +    "description": "__MSG_extensionDescription__",
    11.9 +    "author": "Guido Berhoerster",
   11.10 +    "homepage_url": "https://code.guido-berhoerster.org/addons/firefox-addons/feed-preview/",
   11.11 +    "default_locale": "en",
   11.12 +    "applications": {
   11.13 +        "gecko": {
   11.14 +            "id": "feed-preview@code.guido-berhoerster.org",
   11.15 +            "strict_min_version": "60.0"
   11.16 +        }
   11.17 +    },
   11.18 +    "icons": {
   11.19 +        "48": "icons/feed-preview-48.png",
   11.20 +        "96": "icons/feed-preview-96.png"
   11.21 +    },
   11.22 +    "permissions": [
   11.23 +        "tabs",
   11.24 +        "http://*/*",
   11.25 +        "https://*/*",
   11.26 +        "webRequest",
   11.27 +        "webRequestBlocking"
   11.28 +    ],
   11.29 +    "background": {
   11.30 +        "scripts": [ "background.js" ]
   11.31 +    },
   11.32 +    "content_scripts": [
   11.33 +        {
   11.34 +            "matches": [ "http://*/*", "https://*/*", "file:///*" ],
   11.35 +            "js": [
   11.36 +                "content_scripts/feed-probe.js",
   11.37 +                "content_scripts/feed-preview.js"
   11.38 +            ]
   11.39 +        }
   11.40 +    ],
   11.41 +    "web_accessible_resources": [
   11.42 +        "web_resources/feed-preview.xhtml",
   11.43 +        "web_resources/arrow.svg",
   11.44 +        "web_resources/style/common.css",
   11.45 +        "web_resources/style/entry-content.css",
   11.46 +        "web_resources/style/feed-preview.css",
   11.47 +        "web_resources/style/photon-colors.css"
   11.48 +    ],
   11.49 +    "page_action": {
   11.50 +        "browser_style": true,
   11.51 +        "default_icon": "icons/feed-preview.svg",
   11.52 +        "default_title": "Feeds",
   11.53 +        "default_popup": "popup/feed-selection.html"
   11.54 +    }
   11.55 +}
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/popup/feed-selection.html	Wed Oct 03 23:40:57 2018 +0200
    12.3 @@ -0,0 +1,24 @@
    12.4 +<!doctype html>
    12.5 +<html>
    12.6 +  <head>
    12.7 +    <meta charset="utf-8">
    12.8 +<!--
    12.9 +   Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
   12.10 +
   12.11 +   This Source Code Form is subject to the terms of the Mozilla Public
   12.12 +   License, v. 2.0. If a copy of the MPL was not distributed with this
   12.13 +   file, You can obtain one at http://mozilla.org/MPL/2.0/.
   12.14 +-->
   12.15 +    <script src="feed-selection.js" defer></script>
   12.16 +  </head>
   12.17 +  <body>
   12.18 +  <template id="feed-item-template">
   12.19 +    <div class="panel-list-item" data-href="">
   12.20 +      <div class="icon"><img src="" alt=""></div>
   12.21 +      <div class="text"></div>
   12.22 +    </div>
   12.23 +  </template>
   12.24 +  <div class="panel-section panel-section-list">
   12.25 +  </div>
   12.26 +  </body>
   12.27 +</html>
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/popup/feed-selection.js	Wed Oct 03 23:40:57 2018 +0200
    13.3 @@ -0,0 +1,46 @@
    13.4 +/*
    13.5 + * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
    13.6 + *
    13.7 + * This Source Code Form is subject to the terms of the Mozilla Public
    13.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    13.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
   13.10 + */
   13.11 +
   13.12 +'use strict';
   13.13 +
   13.14 +async function buildFeedSelection() {
   13.15 +    let tabs = await browser.tabs.query({active: true, currentWindow: true});
   13.16 +    let feeds = await browser.runtime.sendMessage(tabs[0].id);
   13.17 +
   13.18 +    let feedListElement = document.querySelector('.panel-section-list');
   13.19 +    feedListElement.addEventListener('click', ev => {
   13.20 +        // find selected list item element and open the feed in a new tab
   13.21 +        for (let element = ev.target; element !== ev.currentTarget;
   13.22 +                element = element.parentElement) {
   13.23 +            if (element.classList.contains('panel-list-item')) {
   13.24 +                browser.tabs.create({url: element.dataset.href});
   13.25 +                break;
   13.26 +            }
   13.27 +        }
   13.28 +        ev.preventDefault();
   13.29 +    });
   13.30 +
   13.31 +    let templateElement = document.querySelector('#feed-item-template');
   13.32 +    for (let feed of feeds) {
   13.33 +        let feedNode = document.importNode(templateElement.content, true);
   13.34 +        feedNode.querySelector('.panel-list-item').dataset.href = feed.href;
   13.35 +
   13.36 +        let prefix = (feed.type === 'application/atom+xml') ? 'Atom Feed' :
   13.37 +                'RSS Feed';
   13.38 +
   13.39 +        let imgNode = feedNode.querySelector('.icon > img');
   13.40 +        imgNode.src = browser.runtime.getURL('icons/feed-preview.svg');
   13.41 +        imgNode.alt = prefix;
   13.42 +
   13.43 +        feedNode.querySelector('.text').textContent =
   13.44 +                `${prefix}: ${feed.title}`;
   13.45 +        feedListElement.appendChild(feedNode);
   13.46 +    }
   13.47 +}
   13.48 +
   13.49 +buildFeedSelection();
    14.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    14.2 +++ b/web_resources/feed-preview.xhtml	Wed Oct 03 23:40:57 2018 +0200
    14.3 @@ -0,0 +1,55 @@
    14.4 +<html xmlns="http://www.w3.org/1999/xhtml">
    14.5 +  <head>
    14.6 +    <meta charset="utf-8"/>
    14.7 +<!--
    14.8 +   Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
    14.9 +
   14.10 +   This Source Code Form is subject to the terms of the Mozilla Public
   14.11 +   License, v. 2.0. If a copy of the MPL was not distributed with this
   14.12 +   file, You can obtain one at http://mozilla.org/MPL/2.0/.
   14.13 +-->
   14.14 +    <meta name="viewport" content="width=device-width, initial-scale=1"/>
   14.15 +    <base href=""/>
   14.16 +    <link rel="stylesheet" href="style/feed-preview.css"/>
   14.17 +    <title></title>
   14.18 +  </head>
   14.19 +  <body>
   14.20 +  <template id="feed-logo-template">
   14.21 +    <img id="feed-logo" src="" alt=""/>
   14.22 +  </template>
   14.23 +  <template id="entry-template">
   14.24 +    <article>
   14.25 +      <details class="entry">
   14.26 +        <summary>
   14.27 +          <header class="entry-header">
   14.28 +            <p class="entry-date"><time></time></p>
   14.29 +          </header>
   14.30 +        </summary>
   14.31 +        <iframe class="entry-content" srcdoc="" title="" sandbox=""
   14.32 +        width="800" height="360"/>
   14.33 +      </details>
   14.34 +    </article>
   14.35 +  </template>
   14.36 +  <template id="entry-title-template">
   14.37 +    <h1 class="entry-title"></h1>
   14.38 +  </template>
   14.39 +  <template id="entry-title-linked-template">
   14.40 +    <h1 class="entry-title"><a class="entry-link" href="" title=""></a></h1>
   14.41 +  </template>
   14.42 +  <template id="entry-files-list-template">
   14.43 +    <footer class="entry-files">
   14.44 +      <h2 class="entry-files-title"></h2>
   14.45 +      <ul class="entry-files-list">
   14.46 +      </ul>
   14.47 +    </footer>
   14.48 +  </template>
   14.49 +  <template id="entry-file-template">
   14.50 +    <li class="entry-file"><a class="entry-file-link" href="" title=""></a>
   14.51 +    <span class="entry-file-info"></span></li>
   14.52 +  </template>
   14.53 +  <header id="feed-header">
   14.54 +    <h1 id="feed-title"></h1>
   14.55 +    <p id="feed-subtitle"></p>
   14.56 +  </header>
   14.57 +  </body>
   14.58 +</html>
    15.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    15.2 +++ b/web_resources/images/arrow.svg	Wed Oct 03 23:40:57 2018 +0200
    15.3 @@ -0,0 +1,10 @@
    15.4 +<!--
    15.5 +Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
    15.6 +
    15.7 +This Source Code Form is subject to the terms of the Mozilla Public
    15.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    15.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   15.10 +-->
   15.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
   15.12 +  <path d="m4 2 6 6-6 6" fill="none" stroke="#0c0c0d" stroke-width="4"/>
   15.13 +</svg>
    16.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    16.2 +++ b/web_resources/style/common.css	Wed Oct 03 23:40:57 2018 +0200
    16.3 @@ -0,0 +1,48 @@
    16.4 +@import url("photon-colors.css");
    16.5 +
    16.6 +html,
    16.7 +body {
    16.8 +  box-sizing: border-box;
    16.9 +  margin: 0;
   16.10 +  font: -moz-desktop;
   16.11 +  font-size: 15px;
   16.12 +  line-height: 1.4em;
   16.13 +  color: var(--grey-90);
   16.14 +}
   16.15 +
   16.16 +h1, h2, h3, h4, h5, h6 {
   16.17 +  line-height: 1.15em;
   16.18 +}
   16.19 +
   16.20 +:link {
   16.21 +  color: var(--blue-50);
   16.22 +  text-decoration: none;
   16.23 +}
   16.24 +
   16.25 +:visited {
   16.26 +  color: var(--blue-50);
   16.27 +  text-decoration: none;
   16.28 +}
   16.29 +
   16.30 +:link:hover,
   16.31 +:link:active,
   16.32 +:visited:hover,
   16.33 +:visited:active {
   16.34 +  text-decoration: underline;
   16.35 +}
   16.36 +
   16.37 +:link:hover {
   16.38 +  color: var(--blue-60);
   16.39 +}
   16.40 +
   16.41 +:link:active {
   16.42 +  color: var(--blue-70);
   16.43 +}
   16.44 +
   16.45 +:visited:hover {
   16.46 +  color: var(--blue-60);
   16.47 +}
   16.48 +
   16.49 +:visited:active {
   16.50 +  color: var(--blue-70);
   16.51 +}
    17.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    17.2 +++ b/web_resources/style/entry-content.css	Wed Oct 03 23:40:57 2018 +0200
    17.3 @@ -0,0 +1,7 @@
    17.4 +@import url("common.css");
    17.5 +
    17.6 +html,
    17.7 +body {
    17.8 +  background: var(--white-100);
    17.9 +  padding: 4px;
   17.10 +}
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/web_resources/style/feed-preview.css	Wed Oct 03 23:40:57 2018 +0200
    18.3 @@ -0,0 +1,100 @@
    18.4 +@import url("common.css");
    18.5 +
    18.6 +html,
    18.7 +body {
    18.8 +  padding: 16px 32px;
    18.9 +  background: var(--grey-10);
   18.10 +}
   18.11 +
   18.12 +#feed-header {
   18.13 +  max-width: 80ch;
   18.14 +  margin: 0 auto;
   18.15 +}
   18.16 +
   18.17 +#feed-logo {
   18.18 +  float: right;
   18.19 +  max-width: 188px;
   18.20 +  max-height: 48px;
   18.21 +  margin: 0 0 8px 8px;
   18.22 +}
   18.23 +
   18.24 +#feed-title {
   18.25 +  font-size: 1.777em;
   18.26 +  margin: 0 0 4px 0;
   18.27 +}
   18.28 +
   18.29 +#feed-subtitle {
   18.30 +  color: var(--grey-50);
   18.31 +  margin: 0 0 16px 0;
   18.32 +}
   18.33 +
   18.34 +.entry {
   18.35 +  clear: both;
   18.36 +  margin: 16px auto;
   18.37 +  padding: 16px;
   18.38 +  max-width: 80ch;
   18.39 +  background: var(--white-100);
   18.40 +  border-radius: 4px;
   18.41 +  box-shadow: 0 1px 2px var(--grey-90-a40);
   18.42 +}
   18.43 +
   18.44 +details.entry > summary {
   18.45 +  display: flex;
   18.46 +  align-items: center;
   18.47 +  list-style-type: none;
   18.48 +  padding: 0 8px;
   18.49 +}
   18.50 +
   18.51 +details.entry > summary:focus {
   18.52 +  outline: none;
   18.53 +}
   18.54 +
   18.55 +details.entry > summary::before {
   18.56 +  content: url('../images/arrow.svg');
   18.57 +  width: 16px;
   18.58 +  height: 16px;
   18.59 +  flex: 0 0 16px;
   18.60 +}
   18.61 +
   18.62 +details.entry[open] > summary {
   18.63 +  margin: 0 0 8px 0;
   18.64 +}
   18.65 +
   18.66 +details.entry[open] > summary::before {
   18.67 +  transform: rotate(90deg);
   18.68 +}
   18.69 +
   18.70 +.entry-header {
   18.71 +  margin: 0 0 0 8px;
   18.72 +}
   18.73 +
   18.74 +.entry-title {
   18.75 +  font-size: 1.333em;
   18.76 +  margin: 0 0 4px 0;
   18.77 +}
   18.78 +
   18.79 +.entry-date {
   18.80 +  color: var(--grey-50);
   18.81 +  margin: 0;
   18.82 +}
   18.83 +
   18.84 +.entry-content {
   18.85 +  width: 100%;
   18.86 +  height: 24em;
   18.87 +  border: 1px solid var(--grey-90-a10);
   18.88 +}
   18.89 +
   18.90 +.entry-files {
   18.91 +  padding: 0 8px;
   18.92 +}
   18.93 +
   18.94 +.entry-files-title {
   18.95 +  font-size: 1em;
   18.96 +  margin: 0 0 4px 0;
   18.97 +}
   18.98 +
   18.99 +.entry-files-list {
  18.100 +  margin: 0;
  18.101 +  padding: 0 0 0 32px;
  18.102 +  color: var(--grey-50);
  18.103 +}
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/web_resources/style/photon-colors.css	Wed Oct 03 23:40:57 2018 +0200
    19.3 @@ -0,0 +1,91 @@
    19.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
    19.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    19.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    19.7 +
    19.8 +/* Photon Colors CSS Variables v3.2.0 */
    19.9 +
   19.10 +:root {
   19.11 +  --magenta-50: #ff1ad9;
   19.12 +  --magenta-60: #ed00b5;
   19.13 +  --magenta-70: #b5007f;
   19.14 +  --magenta-80: #7d004f;
   19.15 +  --magenta-90: #440027;
   19.16 +
   19.17 +  --purple-30: #c069ff;
   19.18 +  --purple-40: #ad3bff;
   19.19 +  --purple-50: #9400ff;
   19.20 +  --purple-60: #8000d7;
   19.21 +  --purple-70: #6200a4;
   19.22 +  --purple-80: #440071;
   19.23 +  --purple-90: #25003e;
   19.24 +
   19.25 +  --blue-40: #45a1ff;
   19.26 +  --blue-50: #0a84ff;
   19.27 +  --blue-50-a30: rgba(10, 132, 255, 0.3);
   19.28 +  --blue-60: #0060df;
   19.29 +  --blue-70: #003eaa;
   19.30 +  --blue-80: #002275;
   19.31 +  --blue-90: #000f40;
   19.32 +
   19.33 +  --teal-50: #00feff;
   19.34 +  --teal-60: #00c8d7;
   19.35 +  --teal-70: #008ea4;
   19.36 +  --teal-80: #005a71;
   19.37 +  --teal-90: #002d3e;
   19.38 +
   19.39 +  --green-50: #30e60b;
   19.40 +  --green-60: #12bc00;
   19.41 +  --green-70: #058b00;
   19.42 +  --green-80: #006504;
   19.43 +  --green-90: #003706;
   19.44 +
   19.45 +  --yellow-50: #ffe900;
   19.46 +  --yellow-60: #d7b600;
   19.47 +  --yellow-70: #a47f00;
   19.48 +  --yellow-80: #715100;
   19.49 +  --yellow-90: #3e2800;
   19.50 +
   19.51 +  --red-50: #ff0039;
   19.52 +  --red-60: #d70022;
   19.53 +  --red-70: #a4000f;
   19.54 +  --red-80: #5a0002;
   19.55 +  --red-90: #3e0200;
   19.56 +
   19.57 +  --orange-50: #ff9400;
   19.58 +  --orange-60: #d76e00;
   19.59 +  --orange-70: #a44900;
   19.60 +  --orange-80: #712b00;
   19.61 +  --orange-90: #3e1300;
   19.62 +
   19.63 +  --grey-10: #f9f9fa;
   19.64 +  --grey-10-a10: rgba(249, 249, 250, 0.1);
   19.65 +  --grey-10-a20: rgba(249, 249, 250, 0.2);
   19.66 +  --grey-10-a40: rgba(249, 249, 250, 0.4);
   19.67 +  --grey-10-a60: rgba(249, 249, 250, 0.6);
   19.68 +  --grey-10-a80: rgba(249, 249, 250, 0.8);
   19.69 +  --grey-20: #ededf0;
   19.70 +  --grey-30: #d7d7db;
   19.71 +  --grey-40: #b1b1b3;
   19.72 +  --grey-50: #737373;
   19.73 +  --grey-60: #4a4a4f;
   19.74 +  --grey-70: #38383d;
   19.75 +  --grey-80: #2a2a2e;
   19.76 +  --grey-90: #0c0c0d;
   19.77 +  --grey-90-a05: rgba(12, 12, 13, 0.05);
   19.78 +  --grey-90-a10: rgba(12, 12, 13, 0.1);
   19.79 +  --grey-90-a20: rgba(12, 12, 13, 0.2);
   19.80 +  --grey-90-a30: rgba(12, 12, 13, 0.3);
   19.81 +  --grey-90-a40: rgba(12, 12, 13, 0.4);
   19.82 +  --grey-90-a50: rgba(12, 12, 13, 0.5);
   19.83 +  --grey-90-a60: rgba(12, 12, 13, 0.6);
   19.84 +  --grey-90-a70: rgba(12, 12, 13, 0.7);
   19.85 +  --grey-90-a80: rgba(12, 12, 13, 0.8);
   19.86 +  --grey-90-a90: rgba(12, 12, 13, 0.9);
   19.87 +
   19.88 +  --ink-70: #363959;
   19.89 +  --ink-80: #202340;
   19.90 +  --ink-90: #0f1126;
   19.91 +
   19.92 +  --white-100: #ffffff;
   19.93 +
   19.94 +}