Mercurial > addons > firefox-addons > feed-preview
comparison js/background.js @ 6:5d7c13e998e9
Create feed previews using a stream filter
Instead of replacing the feed document with an XHTML preview from a content
script after it has already been rendered, create an XHTML preview using a
stream filter before it is passed into the rendering engine and use an XSL
style sheet to convert it to HTML. This has two advantages, firstly it
results in an HTMLDocument with the full HTML DOM available and secondly it
avoids rendering the document twice.
Refactor the feed preview creation and split parsing and rendering into
seperate modules.
author | Guido Berhoerster <guido+feed-preview@berhoerster.name> |
---|---|
date | Thu, 08 Nov 2018 16:30:34 +0100 |
parents | background.js@bc5cc170163c |
children | ff5e5e3eba32 |
comparison
equal
deleted
inserted
replaced
5:341a0f4b7ce0 | 6:5d7c13e998e9 |
---|---|
1 /* | |
2 * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name> | |
3 * | |
4 * This Source Code Form is subject to the terms of the Mozilla Public | |
5 * License, v. 2.0. If a copy of the MPL was not distributed with this | |
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
7 */ | |
8 | |
9 'use strict'; | |
10 | |
11 import * as feedParser from './feed-parser.js'; | |
12 import {renderFeedPreview} from './feed-preview.js'; | |
13 | |
14 const FEED_MAGIC = [ | |
15 '<rss', | |
16 '<feed', | |
17 ...Object.values(feedParser.XMLNS) | |
18 ]; | |
19 var tabsFeeds = new Map(); | |
20 var fetchingFeedPreview = fetch('web_resources/feed-preview.xhtml') | |
21 .then(response => response.text()); | |
22 | |
23 function parseContentType(header) { | |
24 let contentType = { | |
25 mediaType: '', | |
26 charset: 'utf-8' | |
27 }; | |
28 let parts = header.toLowerCase().split(';'); | |
29 contentType.mediaType = parts.shift().trim(); | |
30 for (let parameter of parts) { | |
31 let [, name, value, ] = parameter.trim().split(/([^=]+)="?([^"]*)"?/); | |
32 if (name.toLowerCase() === 'charset') { | |
33 contentType.charset = value.toLowerCase(); | |
34 break; | |
35 } | |
36 } | |
37 | |
38 return contentType; | |
39 } | |
40 | |
41 async function handleFeed(inputText, url) { | |
42 // fast-path: eliminate XML documents which cannot be Atom nor RSS feeds | |
43 let inputTextStart = inputText.substring(0, 512); | |
44 if (!FEED_MAGIC.some(element => inputTextStart.includes(element))) { | |
45 return inputText; | |
46 } | |
47 | |
48 let feed; | |
49 try { | |
50 feed = (new feedParser.FeedParser).parseFromString(inputText, url); | |
51 } catch (e) { | |
52 if (e instanceof feedParser.ParserError || | |
53 e instanceof feedParser.UnsupportedFeedTypeError) { | |
54 // let the browser deal with non-well formed XML or XML documents | |
55 // which are not supported Atom or RSS feeds | |
56 return inputText; | |
57 } | |
58 throw e; | |
59 } | |
60 console.log(`parsed feed ${url}:\n`, feed); | |
61 | |
62 // render the preview document | |
63 let feedPreviewDocument = new DOMParser() | |
64 .parseFromString(await fetchingFeedPreview, 'text/html'); | |
65 renderFeedPreview(feedPreviewDocument, feed); | |
66 | |
67 return new XMLSerializer().serializeToString(feedPreviewDocument); | |
68 } | |
69 | |
70 browser.webRequest.onHeadersReceived.addListener(details => { | |
71 if (details.statusCode !== 200) { | |
72 return {}; | |
73 } | |
74 | |
75 let contentTypeIndex = details.responseHeaders.findIndex(header => | |
76 header.name.toLowerCase() === 'content-type' && | |
77 typeof header.value !== 'undefined'); | |
78 if (contentTypeIndex < 0) { | |
79 // no Content-Type header found | |
80 return {}; | |
81 } | |
82 let headerValue = details.responseHeaders[contentTypeIndex].value | |
83 let contentType = parseContentType(headerValue); | |
84 // until content handlers become available to webextensions | |
85 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1457500) intercept all | |
86 // responses and change the content type from application/atom+xml or | |
87 // application/rss+xml to application/xml which will then be probed for | |
88 // Atom or RSS content | |
89 switch (contentType.mediaType) { | |
90 case 'application/atom+xml': | |
91 case 'application/rss+xml': | |
92 case 'application/rdf+xml': | |
93 case 'application/xml': | |
94 break; | |
95 default: | |
96 // non-XML media type | |
97 return {}; | |
98 } | |
99 console.log(`response is an XML document\n`, | |
100 `media type: ${contentType.mediaType}\n`, | |
101 `charset: ${contentType.charset}`); | |
102 | |
103 let decoder; | |
104 try { | |
105 decoder = new TextDecoder(contentType.charset); | |
106 } catch (e) { | |
107 if (e instanceof RangeError) { | |
108 // unsupported charset | |
109 return {}; | |
110 } else { | |
111 throw e; | |
112 } | |
113 } | |
114 let encoder = new TextEncoder(); | |
115 let inputText = ''; | |
116 let filter = browser.webRequest.filterResponseData(details.requestId); | |
117 filter.addEventListener('data', ev => { | |
118 inputText += decoder.decode(ev.data, {stream: true}); | |
119 }); | |
120 filter.addEventListener('stop', async ev => { | |
121 let result = await handleFeed(inputText, details.url); | |
122 filter.write(encoder.encode(result)); | |
123 filter.close(); | |
124 }); | |
125 | |
126 details.responseHeaders[contentTypeIndex] = { | |
127 name: 'Content-Type', | |
128 value: `application/xml;charset=${contentType.charset}` | |
129 }; | |
130 | |
131 return {responseHeaders: details.responseHeaders}; | |
132 }, | |
133 {urls: ['http://*/*', 'https://*/*'], types: ['main_frame']}, | |
134 ['blocking', 'responseHeaders']); | |
135 | |
136 browser.runtime.onMessage.addListener((request, sender, sendResponse) => { | |
137 let tab = sender.tab; | |
138 if (typeof tab !== 'undefined') { | |
139 // content script sending feeds | |
140 tabsFeeds.set(tab.id, request); | |
141 browser.pageAction.show(tab.id); | |
142 } else { | |
143 // popup querying feeds | |
144 sendResponse(tabsFeeds.get(request)); | |
145 } | |
146 }); | |
147 | |
148 browser.tabs.onUpdated.addListener((id, changeInfo, tab) => { | |
149 if (typeof changeInfo.url === 'undefined') { | |
150 // filter out updates which do not change the URL | |
151 return; | |
152 } | |
153 | |
154 // hide the page action when the URL changes since it is no longer valid, | |
155 // it will be shown again if the content script detects a feed | |
156 browser.pageAction.hide(tab.id); | |
157 }); | |
158 | |
159 browser.tabs.onRemoved.addListener((tabId, removeInfo) => { | |
160 tabsFeeds.delete(tabId); | |
161 }); |