From f9ccc51145164fa447cdf3920f10041f60aa5faf Mon Sep 17 00:00:00 2001 From: mossroy Date: Wed, 6 Jan 2016 16:29:24 +0100 Subject: [PATCH 1/9] Improve the way we start the ServiceWorker, and allow the user to choose between jQuery and ServiceWorker modes. If the user chooses the jQuery mode, we have to disable the ServiceWorker. It's a first step for #136 --- service-worker.js | 20 ++-- www/index.html | 6 ++ www/js/app.js | 236 +++++++++++++++++++++++++++++----------------- 3 files changed, 168 insertions(+), 94 deletions(-) diff --git a/service-worker.js b/service-worker.js index 1b3f7f40..fde62dda 100644 --- a/service-worker.js +++ b/service-worker.js @@ -81,6 +81,15 @@ function(util, utf8) { console.log('Init message received', event.data); outgoingMessagePort = event.ports[0]; console.log('outgoingMessagePort initialized', outgoingMessagePort); + self.addEventListener('fetch', fetchEventListener); + console.log('fetchEventListener enabled'); + } + if (event.data.action === 'disable') { + console.log('Disable message received'); + outgoingMessagePort = null; + console.log('outgoingMessagePort deleted'); + self.removeEventListener('fetch', fetchEventListener); + console.log('fetchEventListener removed'); } }); @@ -93,13 +102,13 @@ function(util, utf8) { var regexpContentUrl = new RegExp(/\/(.)\/(.*[^\/]+)$/); var regexpDummyArticle = new RegExp(/dummyArticle\.html$/); - - self.addEventListener('fetch', function(event) { + + function fetchEventListener(event) { console.log('ServiceWorker handling fetch event for : ' + event.request.url); - + // TODO handle the dummy article more properly if (regexpContentUrl.test(event.request.url) && !regexpDummyArticle.test(event.request.url)) { - + console.log('Asking app.js for a content', event.request.url); event.respondWith(new Promise(function(resolve, reject) { var regexpResult = regexpContentUrl.exec(event.request.url); @@ -163,6 +172,5 @@ function(util, utf8) { } // If event.respondWith() isn't called because this wasn't a request that we want to handle, // then the default request/response behavior will automatically be used. - }); - + } }); diff --git a/www/index.html b/www/index.html index a63dae93..8b408f21 100644 --- a/www/index.html +++ b/www/index.html @@ -207,6 +207,12 @@
Please select the archive you want to use :
Click here to rescan your SD Cards and internal memory +
+ Content injection mode :
+ +
+ +

diff --git a/www/js/app.js b/www/js/app.js index 11130688..0603a5d4 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -227,24 +227,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction $('#geolocationProgress').hide(); $('#articleContent').hide(); $('#searchingForTitles').hide(); - // Refresh the ServiceWorker status (in case it has been unregistered) - if (isServiceWorkerAvailable()) { - if (isServiceWorkerReady()) { - $('#serviceWorkerStatus').html("ServiceWorker API available, and registered"); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiAvailable"); - } - else { - $('#serviceWorkerStatus').html("ServiceWorker API available, but not registered"); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiUnavailable"); - } - } - else { - $('#serviceWorkerStatus').html("ServiceWorker API unavailable"); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiUnavailable"); - } + refreshAPIStatus(); return false; }); $('#btnAbout').on('click', function(e) { @@ -271,6 +254,140 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction $('#searchingForTitles').hide(); return false; }); + $('input:radio[name=contentInjectionMode]').on('change', function(e) { + // Do the necessary to enable or disable the Service Worker + setContentInjectionMode(this.value); + checkSelectedArchiveCompatibilityWithInjectionMode(); + }); + + /** + * Displays of refreshes the API status shown to the user + */ + function refreshAPIStatus() { + if (isMessageChannelAvailable()) { + $('#messageChannelStatus').html("MessageChannel API available"); + $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") + .addClass("apiAvailable"); + } else { + $('#messageChannelStatus').html("MessageChannel API unavailable"); + $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") + .addClass("apiUnavailable"); + } + if (isServiceWorkerAvailable()) { + if (isServiceWorkerReady()) { + $('#serviceWorkerStatus').html("ServiceWorker API available, and registered"); + $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") + .addClass("apiAvailable"); + } else { + $('#serviceWorkerStatus').html("ServiceWorker API available, but not registered"); + $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") + .addClass("apiUnavailable"); + } + } else { + $('#serviceWorkerStatus').html("ServiceWorker API unavailable"); + $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") + .addClass("apiUnavailable"); + } + } + + var contentInjectionMode; + + /** + * Sets the given injection mode. + * This involves registering (or re-enabling) the Service Worker if necessary + * It also refreshes the API status for the user afterwards. + * + * @param {String} value The chosen content injection mode : 'jquery' or 'serviceworker' + */ + function setContentInjectionMode(value) { + if (value === 'jquery') { + if (isServiceWorkerReady()) { + // We need to disable the ServiceWorker + // Unregistering it does not seem to work as expected : the ServiceWorker + // is indeed unregistered but still active... + // So we have to disable it manually (even if it's still registered and active) + navigator.serviceWorker.controller.postMessage({'action': 'disable'}); + messageChannel = null; + } + refreshAPIStatus(); + } else if (value === 'serviceworker') { + if (!isServiceWorkerAvailable()) { + alert("The ServiceWorker API is not available on your device. Falling back to JQuery mode"); + setContentInjectionMode('jquery'); + return; + } + if (!isMessageChannelAvailable()) { + alert("The MessageChannel API is not available on your device. Falling back to JQuery mode"); + setContentInjectionMode('jquery'); + return; + } + + if (!messageChannel) { + // Let's create the messageChannel for the 2-way communication + // with the Service Worker + messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = handleMessageChannelMessage; + } + + if (!isServiceWorkerReady()) { + $('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it..."); + navigator.serviceWorker.register('../service-worker.js').then(function (reg) { + console.log('serviceWorker registered', reg); + serviceWorkerRegistration = reg; + refreshAPIStatus(); + + // We need to wait for the ServiceWorker to be activated + // before sending the first init message + var serviceWorker; + if (reg.installing) { + serviceWorker = reg.installing; + } else if (reg.waiting) { + serviceWorker = reg.waiting; + } else if (reg.active) { + serviceWorker = reg.active; + } + serviceWorker.addEventListener('statechange', function(statechangeevent) { + if (statechangeevent.target.state === 'activated') { + console.log("try to post an init message to ServiceWorker"); + navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]); + console.log("init message sent to ServiceWorker"); + } + }); + }, function (err) { + console.error('error while registering serviceWorker', err); + refreshAPIStatus(); + }); + } else { + console.log("try to re-post an init message to ServiceWorker, to re-enable it in case it was disabled"); + navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]); + console.log("init message sent to ServiceWorker"); + } + } + $('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').attr('checked', true); + contentInjectionMode = value; + // Save the value in a cookie, so that to be able to keep it after a reload/restart + cookies.setItem('lastContentInjectionMode', value, Infinity); + } + + /** + * Checks if the archive selected by the user is compatible + * with the injection mode, and warn the user if it's not + * @returns {Boolean} true if they're compatible + */ + function checkSelectedArchiveCompatibilityWithInjectionMode() { + if (selectedArchive.needsWikimediaCSS() && contentInjectionMode === 'serviceworker') { + alert('You seem to want to use ServiceWorker mode for an Evopedia archive : this is not supported. Please use the JQuery mode or use a ZIM file'); + $("#btnConfigure").click(); + return false; + } + return true; + } + + // At launch, we try to set the last content injection mode (stored in a cookie) + var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode'); + if (lastContentInjectionMode) { + setContentInjectionMode(lastContentInjectionMode); + } var serviceWorkerRegistration = null; @@ -305,42 +422,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction * @returns {Boolean} */ function isServiceWorkerReady() { - return (serviceWorkerRegistration !== null); + // Return true if the serviceWorkerRegistration is not null and not undefined + return (serviceWorkerRegistration); } - if (isServiceWorkerAvailable()) { - $('#serviceWorkerStatus').html("ServiceWorker API available : trying to register it..."); - navigator.serviceWorker.register('../service-worker.js').then(function(reg) { - console.log('serviceWorker registered', reg); - serviceWorkerRegistration = reg; - $('#serviceWorkerStatus').html("ServiceWorker API available, and registered"); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiAvailable"); - }, function(err) { - console.error('error while registering serviceWorker', err); - $('#serviceWorkerStatus').html("ServiceWorker API available, but unable to register : " + err); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiUnavailable"); - }); - } - else { - console.log("serviceWorker API not available"); - $('#serviceWorkerStatus').html("ServiceWorker API unavailable"); - $('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiUnavailable"); - } - if (isMessageChannelAvailable()) { - $('#messageChannelStatus').html("MessageChannel API available"); - $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiAvailable"); - } - else { - $('#messageChannelStatus').html("MessageChannel API unavailable"); - $('#messageChannelStatus').removeClass("apiAvailable apiUnavailable") - .addClass("apiUnavailable"); - } - - // Detect if DeviceStorage is available /** * * @type Array. @@ -532,8 +617,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction } selectedArchive = backend.loadArchiveFromDeviceStorage(selectedStorage, archiveDirectory); cookies.setItem("lastSelectedArchive", archiveDirectory, Infinity); - // The archive is set : go back to home page to start searching - $("#btnHome").click(); + if (checkSelectedArchiveCompatibilityWithInjectionMode()) { + // The archive is set : go back to home page to start searching + $("#btnHome").click(); + } } } @@ -550,8 +637,10 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction */ function setLocalArchiveFromFileSelect() { selectedArchive = backend.loadArchiveFromFiles(document.getElementById('archiveFiles').files); - // The archive is set : go back to home page to start searching - $("#btnHome").click(); + if (checkSelectedArchiveCompatibilityWithInjectionMode()) { + // The archive is set : go back to home page to start searching + $("#btnHome").click(); + } } /** @@ -745,13 +834,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction } } - if (isMessageChannelAvailable()) { - // Let's instanciate the messageChannel where the ServiceWorker can ask for contents - // NB : note that we use the var keyword here (and not let), - // so that the scope of the variable is the whole file - var messageChannel = new MessageChannel(); - messageChannel.port1.onmessage = handleMessageChannelMessage; - } + var messageChannel; /** * Function that handles a message of the messageChannel. @@ -811,25 +894,6 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction $("#articleContent").show(); // Scroll the iframe to its top $("#articleContent").contents().scrollTop(0); - - if (isServiceWorkerReady()) { - // TODO : We do not use Service Workers on Evopedia archives, for now - // Maybe it would be worth trying to enable them in the future? - if (selectedArchive.needsWikimediaCSS()) { - // Let's unregister the ServiceWorker - serviceWorkerRegistration.unregister().then(function() {serviceWorkerRegistration = null;}); - } - else { - // TODO : for testing : this initialization should be done earlier, - // as soon as the ServiceWorker is ready. - // This can probably been done by listening to state change : - // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange - console.log("try to post an init message to ServiceWorker"); - console.log("messageChannel :", messageChannel); - navigator.serviceWorker.controller.postMessage({'action': 'init'}, [messageChannel.port2]); - console.log("init message sent to ServiceWorker"); - } - } // Apply Mediawiki CSS only when it's an Evopedia archive if (selectedArchive.needsWikimediaCSS() === true) { @@ -838,17 +902,13 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction var currentPath = regexpPath.exec(currentHref)[1]; $('#articleContent').contents().find('head').append(""); } - else { - // TODO temporary test to inject CSS inside the iframe - $('#articleContent').contents().find('head').empty(); - $('#articleContent').contents().find('head').append(""); - } + // Display the article inside the web page. $('#articleContent').contents().find('body').html(htmlArticle); // If the ServiceWorker is not useable, we need to fallback to parse the DOM // to inject math images, and replace some links with javascript calls - if (selectedArchive.needsWikimediaCSS() || !isServiceWorkerReady() || !isMessageChannelAvailable()) { + if (contentInjectionMode === 'jquery') { // Convert links into javascript calls $('#articleContent').contents().find('body').find('a').each(function() { From a4feca993084908ff7597d46f5c6e10283f0492b Mon Sep 17 00:00:00 2001 From: mossroy Date: Wed, 6 Jan 2016 18:39:40 +0100 Subject: [PATCH 2/9] In jQuery mode, display images, javascript and CSS dependencies. Images seem to work fine. I could not test JS and CSS for now See #136 --- service-worker.js | 26 ----------------------- www/js/app.js | 53 ++++++++++++++++++++++++++++++++++++++++++++-- www/js/lib/util.js | 49 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 29 deletions(-) diff --git a/service-worker.js b/service-worker.js index fde62dda..aec744bd 100644 --- a/service-worker.js +++ b/service-worker.js @@ -25,32 +25,6 @@ // TODO : remove requirejs if it's really useless here importScripts('./www/js/lib/require.js'); -/** - * From https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript - */ -function b64toBlob(b64Data, contentType, sliceSize) { - contentType = contentType || ''; - sliceSize = sliceSize || 512; - - var byteCharacters = atob(b64Data); - var byteArrays = []; - - for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { - var slice = byteCharacters.slice(offset, offset + sliceSize); - - var byteNumbers = new Array(slice.length); - for (var i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - var byteArray = new Uint8Array(byteNumbers); - - byteArrays.push(byteArray); - } - - var blob = new Blob(byteArrays, {type: contentType}); - return blob; -} self.addEventListener('install', function(event) { event.waitUntil(self.skipWaiting()); diff --git a/www/js/app.js b/www/js/app.js index 0603a5d4..ad255539 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -375,7 +375,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction * @returns {Boolean} true if they're compatible */ function checkSelectedArchiveCompatibilityWithInjectionMode() { - if (selectedArchive.needsWikimediaCSS() && contentInjectionMode === 'serviceworker') { + if (selectedArchive && selectedArchive.needsWikimediaCSS() && contentInjectionMode === 'serviceworker') { alert('You seem to want to use ServiceWorker mode for an Evopedia archive : this is not supported. Please use the JQuery mode or use a ZIM file'); $("#btnConfigure").click(); return false; @@ -881,6 +881,8 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction var regexpImageLink = /^.?\/?[^:]+:(.*)/; var regexpMathImageUrl = /^\/math.*\/([0-9a-f]{32})\.png$/; var regexpPath = /^(.*\/)[^\/]+$/; + var regexpImageUrl = /^\.\.\/I\/(.*)$/; + var regexpMetadataUrl = /^\.\.\/-\/(.*)$/; /** * Display the the given HTML article in the web page, @@ -969,14 +971,61 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction }); } - // Load math images + // Load images $('#articleContent').contents().find('body').find('img').each(function() { var image = $(this); var m = image.attr("src").match(regexpMathImageUrl); if (m) { + // It's a math image (Evopedia archive) selectedArchive.loadMathImage(m[1], function(data) { image.attr("src", 'data:image/png;base64,' + data); }); + } else { + // It's a standard image contained in the ZIM file + var imageMatch = image.attr("src").match(regexpImageUrl); + if (imageMatch) { + selectedArchive.getTitleByName(imageMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + // TODO : add the complete MIME-type of the image (as read from the ZIM file) + image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content)); + }); + }).fail(function () { + console.error("could not find title for image:" + imageMatch[1]); + }); + } + } + }); + + // Load CSS content + $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { + var link = $(this); + var hrefMatch = link.attr("href").match(regexpMetadataUrl); + if (hrefMatch) { + // It's a CSS file contained in the ZIM file + selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + }); + }).fail(function () { + console.error("could not find title for CSS : " + hrefMatch[1]); + }); + } + }); + + // Load Javascript content + $('#articleContent').contents().find('body').find('script').each(function() { + var script = $(this); + var srcMatch = script.attr("src").match(regexpMetadataUrl); + // TODO check that the type of the script is text/javascript or application/javascript + if (srcMatch) { + // It's a Javascript file contained in the ZIM file + selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + }); + }).fail(function () { + console.error("could not find title for javascript : " + srcMatch[1]); + }); } }); } diff --git a/www/js/lib/util.js b/www/js/lib/util.js index 02f3a876..ff1bf9d8 100644 --- a/www/js/lib/util.js +++ b/www/js/lib/util.js @@ -159,6 +159,51 @@ define(['q'], function(q) { return mid; }); }; + + /** + * Converts a Base64 Content to a Blob + * From https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript + * + * @param {String} b64Data Base64-encoded data + * @param {String} contentType + * @param {Integer} sliceSize + * @returns {Blob} + */ + function b64toBlob(b64Data, contentType, sliceSize) { + contentType = contentType || ''; + sliceSize = sliceSize || 512; + + var byteCharacters = atob(b64Data); + var byteArrays = []; + + for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { + var slice = byteCharacters.slice(offset, offset + sliceSize); + + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + var byteArray = new Uint8Array(byteNumbers); + + byteArrays.push(byteArray); + } + + var blob = new Blob(byteArrays, {type: contentType}); + return blob; + } + + /** + * Converts a UInt Array to a UTF-8 encoded string + * + * @param {UIntArray} uintArray + * @returns {String} + */ + function uintToString(uintArray) { + var encodedString = String.fromCharCode.apply(null, uintArray), + decodedString = decodeURIComponent(escape(encodedString)); + return decodedString; + } /** * Functions and classes exposed by this module @@ -171,6 +216,8 @@ define(['q'], function(q) { uint8ArrayToHex : uint8ArrayToHex, uint8ArrayToBase64 : uint8ArrayToBase64, readFileSlice : readFileSlice, - binarySearch: binarySearch + binarySearch: binarySearch, + b64toBlob: b64toBlob, + uintToString: uintToString }; }); From 369f8a9b0fd7ec421765cf01d4f6690da0446e25 Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 16:39:28 +0100 Subject: [PATCH 3/9] Fixes for jQuery/ServiceWorker switch, jQuery mode, and workaround for javascript/css content in ServiceWorker mode --- service-worker.js | 14 ++- www/js/app.js | 260 ++++++++++++++++++++++++---------------------- 2 files changed, 149 insertions(+), 125 deletions(-) diff --git a/service-worker.js b/service-worker.js index aec744bd..450e4c5e 100644 --- a/service-worker.js +++ b/service-worker.js @@ -113,7 +113,19 @@ function(util, utf8) { else if (regexpCSS.test(titleName)) { contentType = 'image/css'; } - reject("temporarily refuse javascript and css dependencies"); + var responseInit = { + status: 200, + statusText: 'OK', + headers: { + 'Content-Type': contentType + } + }; + + var httpResponse = new Response(';', responseInit); + + // TODO : temporary before the backend actually sends a proper content + resolve(httpResponse); + return; } // Let's instanciate a new messageChannel, to allow app.s to give us the content diff --git a/www/js/app.js b/www/js/app.js index ad255539..eb6ed495 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -363,7 +363,8 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction console.log("init message sent to ServiceWorker"); } } - $('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').attr('checked', true); + $('input:radio[name=contentInjectionMode]').prop('checked', false); + $('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true); contentInjectionMode = value; // Save the value in a cookie, so that to be able to keep it after a reload/restart cookies.setItem('lastContentInjectionMode', value, Infinity); @@ -388,6 +389,9 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction if (lastContentInjectionMode) { setContentInjectionMode(lastContentInjectionMode); } + else { + setContentInjectionMode('jquery'); + } var serviceWorkerRegistration = null; @@ -790,13 +794,13 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction $('#titleListHeaderMessage').empty(); $('#suggestEnlargeMaxDistance').hide(); $('#suggestReduceMaxDistance').hide(); + $("#prefix").val(""); findTitleFromTitleIdAndLaunchArticleRead(titleId); var title = selectedArchive.parseTitleId(titleId); pushBrowserHistoryState(title.name()); - $("#prefix").val(""); return false; } - + /** * Creates an instance of title from given titleId (including resolving redirects), @@ -897,137 +901,145 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction // Scroll the iframe to its top $("#articleContent").contents().scrollTop(0); - // Apply Mediawiki CSS only when it's an Evopedia archive - if (selectedArchive.needsWikimediaCSS() === true) { - $('#articleContent').contents().find('head').empty(); - var currentHref = $(location).attr('href'); - var currentPath = regexpPath.exec(currentHref)[1]; - $('#articleContent').contents().find('head').append(""); - } - // Display the article inside the web page. - $('#articleContent').contents().find('body').html(htmlArticle); + var ifrm = document.getElementById('articleContent'); + ifrm = (ifrm.contentWindow) ? ifrm.contentWindow : (ifrm.contentDocument.document) ? ifrm.contentDocument.document : ifrm.contentDocument; + ifrm.document.open(); + ifrm.document.write(htmlArticle); + ifrm.document.close(); - // If the ServiceWorker is not useable, we need to fallback to parse the DOM - // to inject math images, and replace some links with javascript calls - if (contentInjectionMode === 'jquery') { + //$(document.getElementById('#articleContent').contentWindow.document).load(function () { + $('iframe#articleContent').load(function() { + // Apply Mediawiki CSS only when it's an Evopedia archive + if (selectedArchive.needsWikimediaCSS() === true) { + var currentHref = $(location).attr('href'); + var currentPath = regexpPath.exec(currentHref)[1]; + $('#articleContent').contents().find('head').append(""); + } - // Convert links into javascript calls - $('#articleContent').contents().find('body').find('a').each(function() { - // Store current link's url - var url = $(this).attr("href"); - if (url === null || url === undefined) { - return; - } - var lowerCaseUrl = url.toLowerCase(); - var cssClass = $(this).attr("class"); + // If the ServiceWorker is not useable, we need to fallback to parse the DOM + // to inject math images, and replace some links with javascript calls + if (contentInjectionMode === 'jquery') { - if (cssClass === "new") { - // It's a link to a missing article : display a message - $(this).on('click', function(e) { - alert("Missing article in Wikipedia"); - return false; - }); - } - else if (url.slice(0, 1) === "#") { - // It's an anchor link : do nothing - } - else if (url.substring(0, 4) === "http") { - // It's an external link : open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexpOtherLanguage)) { - // It's a link to another language : change the URL to the online version of wikipedia - // The regular expression extracts $1 as the language, and $2 as the title name - var onlineWikipediaUrl = url.replace(regexpOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); - $(this).attr("href", onlineWikipediaUrl); - // Open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexpImageLink) - && (util.endsWith(lowerCaseUrl, ".png") - || util.endsWith(lowerCaseUrl, ".svg") - || util.endsWith(lowerCaseUrl, ".jpg") - || util.endsWith(lowerCaseUrl, ".jpeg"))) { - // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab - var onlineWikipediaUrl = url.replace(regexpImageLink, "https://" + selectedArchive._language + ".wikipedia.org/wiki/File:$1"); - $(this).attr("href", onlineWikipediaUrl); - $(this).attr("target", "_blank"); - } - else { - // It's a link to another article - // Add an onclick event to go to this article - // instead of following the link - if (url.length>=2 && url.substring(0, 2) === "./") { - url = url.substring(2); + // Convert links into javascript calls + $('#articleContent').contents().find('body').find('a').each(function() { + // Store current link's url + var url = $(this).attr("href"); + if (url === null || url === undefined) { + return; } - $(this).on('click', function(e) { - var titleName = decodeURIComponent(url); - pushBrowserHistoryState(titleName); - goToArticle(titleName); - return false; - }); - } - }); - } + var lowerCaseUrl = url.toLowerCase(); + var cssClass = $(this).attr("class"); - // Load images - $('#articleContent').contents().find('body').find('img').each(function() { - var image = $(this); - var m = image.attr("src").match(regexpMathImageUrl); - if (m) { - // It's a math image (Evopedia archive) - selectedArchive.loadMathImage(m[1], function(data) { - image.attr("src", 'data:image/png;base64,' + data); - }); - } else { - // It's a standard image contained in the ZIM file - var imageMatch = image.attr("src").match(regexpImageUrl); - if (imageMatch) { - selectedArchive.getTitleByName(imageMatch[1]).then(function(title) { - selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - // TODO : add the complete MIME-type of the image (as read from the ZIM file) - image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content)); + if (cssClass === "new") { + // It's a link to a missing article : display a message + $(this).on('click', function(e) { + alert("Missing article in Wikipedia"); + return false; }); - }).fail(function () { - console.error("could not find title for image:" + imageMatch[1]); - }); - } - } - }); - - // Load CSS content - $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { - var link = $(this); - var hrefMatch = link.attr("href").match(regexpMetadataUrl); - if (hrefMatch) { - // It's a CSS file contained in the ZIM file - selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { - selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); - }); - }).fail(function () { - console.error("could not find title for CSS : " + hrefMatch[1]); + } + else if (url.slice(0, 1) === "#") { + // It's an anchor link : do nothing + } + else if (url.substring(0, 4) === "http") { + // It's an external link : open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexpOtherLanguage)) { + // It's a link to another language : change the URL to the online version of wikipedia + // The regular expression extracts $1 as the language, and $2 as the title name + var onlineWikipediaUrl = url.replace(regexpOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); + $(this).attr("href", onlineWikipediaUrl); + // Open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexpImageLink) + && (util.endsWith(lowerCaseUrl, ".png") + || util.endsWith(lowerCaseUrl, ".svg") + || util.endsWith(lowerCaseUrl, ".jpg") + || util.endsWith(lowerCaseUrl, ".jpeg"))) { + // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab + var onlineWikipediaUrl = url.replace(regexpImageLink, "https://" + selectedArchive._language + ".wikipedia.org/wiki/File:$1"); + $(this).attr("href", onlineWikipediaUrl); + $(this).attr("target", "_blank"); + } + else { + // It's a link to another article + // Add an onclick event to go to this article + // instead of following the link + if (url.length>=2 && url.substring(0, 2) === "./") { + url = url.substring(2); + } + $(this).on('click', function(e) { + var titleName = decodeURIComponent(url); + pushBrowserHistoryState(titleName); + goToArticle(titleName); + return false; + }); + } }); - } - }); - - // Load Javascript content - $('#articleContent').contents().find('body').find('script').each(function() { - var script = $(this); - var srcMatch = script.attr("src").match(regexpMetadataUrl); - // TODO check that the type of the script is text/javascript or application/javascript - if (srcMatch) { - // It's a Javascript file contained in the ZIM file - selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { - selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); - }); - }).fail(function () { - console.error("could not find title for javascript : " + srcMatch[1]); + + // Load images + $('#articleContent').contents().find('body').find('img').each(function() { + var image = $(this); + var m = image.attr("src").match(regexpMathImageUrl); + if (m) { + // It's a math image (Evopedia archive) + selectedArchive.loadMathImage(m[1], function(data) { + image.attr("src", 'data:image/png;base64,' + data); + }); + } else { + // It's a standard image contained in the ZIM file + var imageMatch = image.attr("src").match(regexpImageUrl); + if (imageMatch) { + selectedArchive.getTitleByName(imageMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + // TODO : add the complete MIME-type of the image (as read from the ZIM file) + image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content)); + }); + }).fail(function () { + console.error("could not find title for image:" + imageMatch[1]); + }); + } + } }); + + // Load CSS content + $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { + var link = $(this); + var hrefMatch = link.attr("href").match(regexpMetadataUrl); + if (hrefMatch) { + // It's a CSS file contained in the ZIM file + selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + }); + }).fail(function () { + console.error("could not find title for CSS : " + hrefMatch[1]); + }); + } + }); + + // Load Javascript content + $('#articleContent').contents().find('body').find('script').each(function() { + var script = $(this); + var srcMatch = script.attr("src").match(regexpMetadataUrl); + // TODO check that the type of the script is text/javascript or application/javascript + if (srcMatch) { + // It's a Javascript file contained in the ZIM file + selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + }); + }).fail(function () { + console.error("could not find title for javascript : " + srcMatch[1]); + }); + } + }); + } - }); + + }); } /** From a1793121ae15465098fdda876ccc6d4b673614fe Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 17:42:37 +0100 Subject: [PATCH 4/9] Fix merge error + minor comments improvements --- service-worker.js | 59 ++++++++++++++++++++++------------------------- www/js/app.js | 12 +++++----- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/service-worker.js b/service-worker.js index 1171954e..450e4c5e 100644 --- a/service-worker.js +++ b/service-worker.js @@ -25,32 +25,6 @@ // TODO : remove requirejs if it's really useless here importScripts('./www/js/lib/require.js'); -/** - * From https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript - */ -function b64toBlob(b64Data, contentType, sliceSize) { - contentType = contentType || ''; - sliceSize = sliceSize || 512; - - var byteCharacters = atob(b64Data); - var byteArrays = []; - - for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { - var slice = byteCharacters.slice(offset, offset + sliceSize); - - var byteNumbers = new Array(slice.length); - for (var i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - var byteArray = new Uint8Array(byteNumbers); - - byteArrays.push(byteArray); - } - - var blob = new Blob(byteArrays, {type: contentType}); - return blob; -} self.addEventListener('install', function(event) { event.waitUntil(self.skipWaiting()); @@ -81,6 +55,15 @@ function(util, utf8) { console.log('Init message received', event.data); outgoingMessagePort = event.ports[0]; console.log('outgoingMessagePort initialized', outgoingMessagePort); + self.addEventListener('fetch', fetchEventListener); + console.log('fetchEventListener enabled'); + } + if (event.data.action === 'disable') { + console.log('Disable message received'); + outgoingMessagePort = null; + console.log('outgoingMessagePort deleted'); + self.removeEventListener('fetch', fetchEventListener); + console.log('fetchEventListener removed'); } }); @@ -93,13 +76,13 @@ function(util, utf8) { var regexpContentUrl = new RegExp(/\/(.)\/(.*[^\/]+)$/); var regexpDummyArticle = new RegExp(/dummyArticle\.html$/); - - self.addEventListener('fetch', function(event) { + + function fetchEventListener(event) { console.log('ServiceWorker handling fetch event for : ' + event.request.url); - + // TODO handle the dummy article more properly if (regexpContentUrl.test(event.request.url) && !regexpDummyArticle.test(event.request.url)) { - + console.log('Asking app.js for a content', event.request.url); event.respondWith(new Promise(function(resolve, reject) { var regexpResult = regexpContentUrl.exec(event.request.url); @@ -130,6 +113,19 @@ function(util, utf8) { else if (regexpCSS.test(titleName)) { contentType = 'image/css'; } + var responseInit = { + status: 200, + statusText: 'OK', + headers: { + 'Content-Type': contentType + } + }; + + var httpResponse = new Response(';', responseInit); + + // TODO : temporary before the backend actually sends a proper content + resolve(httpResponse); + return; } // Let's instanciate a new messageChannel, to allow app.s to give us the content @@ -162,6 +158,5 @@ function(util, utf8) { } // If event.respondWith() isn't called because this wasn't a request that we want to handle, // then the default request/response behavior will automatically be used. - }); - + } }); diff --git a/www/js/app.js b/www/js/app.js index a2b8ed7f..98ff3f47 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -902,13 +902,13 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction $("#articleContent").contents().scrollTop(0); // Display the article inside the web page. - var ifrm = document.getElementById('articleContent'); - ifrm = (ifrm.contentWindow) ? ifrm.contentWindow : (ifrm.contentDocument.document) ? ifrm.contentDocument.document : ifrm.contentDocument; - ifrm.document.open(); - ifrm.document.write(htmlArticle); - ifrm.document.close(); + var articleContentIFrame = document.getElementById('articleContent'); + articleContentIFrame = (articleContentIFrame.contentWindow) ? articleContentIFrame.contentWindow : (articleContentIFrame.contentDocument.document) ? articleContentIFrame.contentDocument.document : articleContentIFrame.contentDocument; + articleContentIFrame.document.open(); + articleContentIFrame.document.write(htmlArticle); + articleContentIFrame.document.close(); - //$(document.getElementById('#articleContent').contentWindow.document).load(function () { + // When the IFrame content is loaded, we can parse it $('iframe#articleContent').load(function() { // Apply Mediawiki CSS only when it's an Evopedia archive if (selectedArchive.needsWikimediaCSS() === true) { From 799dc281be6e6713f24a7d927f270679468ccfee Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 17:58:38 +0100 Subject: [PATCH 5/9] Handle the default namespace for articles, in the ServiceWorker --- service-worker.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/service-worker.js b/service-worker.js index 450e4c5e..b3891f92 100644 --- a/service-worker.js +++ b/service-worker.js @@ -42,9 +42,9 @@ self.addEventListener('activate', function(event) { require({ baseUrl: "./www/js/lib/" }, -["util", "utf8"], +["util"], -function(util, utf8) { +function(util) { console.log("ServiceWorker startup"); @@ -74,21 +74,34 @@ function(util, utf8) { var regexpJS = new RegExp(/\.js/i); var regexpCSS = new RegExp(/\.css$/i); - var regexpContentUrl = new RegExp(/\/(.)\/(.*[^\/]+)$/); + var regexpContentUrlWithNamespace = new RegExp(/\/(.)\/(.*[^\/]+)$/); + var regexpContentUrlWithoutNamespace = new RegExp(/^(.*[^\/]+)$/); var regexpDummyArticle = new RegExp(/dummyArticle\.html$/); function fetchEventListener(event) { console.log('ServiceWorker handling fetch event for : ' + event.request.url); // TODO handle the dummy article more properly - if (regexpContentUrl.test(event.request.url) && !regexpDummyArticle.test(event.request.url)) { + if ((regexpContentUrlWithNamespace.test(event.request.url) + || regexpContentUrlWithoutNamespace.test(event.request.url)) + && !regexpDummyArticle.test(event.request.url)) { console.log('Asking app.js for a content', event.request.url); event.respondWith(new Promise(function(resolve, reject) { - var regexpResult = regexpContentUrl.exec(event.request.url); - var nameSpace = regexpResult[1]; - var titleName = regexpResult[2]; + var nameSpace; + var titleName; var contentType; + if (regexpContentUrlWithoutNamespace.test(event.request.url)) { + // When the request URL is in the same folder, + // it means it's a link to an article (namespace A) + var regexpResult = regexpContentUrlWithoutNamespace.exec(event.request.url); + nameSpace = 'A'; + titleName = regexpResult[1]; + } else { + var regexpResult = regexpContentUrlWithNamespace.exec(event.request.url); + nameSpace = regexpResult[1]; + titleName = regexpResult[2]; + } // The namespace defines the type of content. See http://www.openzim.org/wiki/ZIM_file_format#Namespaces // TODO : read the contentType from the ZIM file instead of hard-coding it here From 71b1b2a9b5fa60d53a20fe2d1a14d40cc96112ce Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 18:39:07 +0100 Subject: [PATCH 6/9] Fix to reflect the need of the namespace to read in the ZIM backend. --- service-worker.js | 2 +- www/js/app.js | 246 +++++++++++++++++++-------------------- www/js/lib/zimArchive.js | 6 + 3 files changed, 127 insertions(+), 127 deletions(-) diff --git a/service-worker.js b/service-worker.js index b3891f92..f41438d6 100644 --- a/service-worker.js +++ b/service-worker.js @@ -75,7 +75,7 @@ function(util) { var regexpCSS = new RegExp(/\.css$/i); var regexpContentUrlWithNamespace = new RegExp(/\/(.)\/(.*[^\/]+)$/); - var regexpContentUrlWithoutNamespace = new RegExp(/^(.*[^\/]+)$/); + var regexpContentUrlWithoutNamespace = new RegExp(/^([^\/]+)$/); var regexpDummyArticle = new RegExp(/dummyArticle\.html$/); function fetchEventListener(event) { diff --git a/www/js/app.js b/www/js/app.js index 40adf494..d38e9ca1 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -884,8 +884,8 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction var regexpImageLink = /^.?\/?[^:]+:(.*)/; var regexpMathImageUrl = /^\/math.*\/([0-9a-f]{32})\.png$/; var regexpPath = /^(.*\/)[^\/]+$/; - var regexpImageUrl = /^\.\.\/I\/(.*)$/; - var regexpMetadataUrl = /^\.\.\/-\/(.*)$/; + var regexpImageUrl = /^\.\.\/(I\/.*)$/; + var regexpMetadataUrl = /^\.\.\/(-\/.*)$/; /** * Display the the given HTML article in the web page, @@ -900,145 +900,139 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction // Scroll the iframe to its top $("#articleContent").contents().scrollTop(0); + // Apply Mediawiki CSS only when it's an Evopedia archive + if (selectedArchive.needsWikimediaCSS() === true) { + $('#articleContent').contents().find('head').empty(); + var currentHref = $(location).attr('href'); + var currentPath = regexpPath.exec(currentHref)[1]; + $('#articleContent').contents().find('head').append(""); + } + // Display the article inside the web page. - var articleContentIFrame = document.getElementById('articleContent'); - articleContentIFrame = (articleContentIFrame.contentWindow) ? articleContentIFrame.contentWindow : (articleContentIFrame.contentDocument.document) ? articleContentIFrame.contentDocument.document : articleContentIFrame.contentDocument; - articleContentIFrame.document.open(); - articleContentIFrame.document.write(htmlArticle); - articleContentIFrame.document.close(); + $('#articleContent').contents().find('body').html(htmlArticle); - // When the IFrame content is loaded, we can parse it - $('iframe#articleContent').load(function() { - // Apply Mediawiki CSS only when it's an Evopedia archive - if (selectedArchive.needsWikimediaCSS() === true) { - var currentHref = $(location).attr('href'); - var currentPath = regexpPath.exec(currentHref)[1]; - $('#articleContent').contents().find('head').append(""); - } + + // If the ServiceWorker is not useable, we need to fallback to parse the DOM + // to inject math images, and replace some links with javascript calls + if (contentInjectionMode === 'jquery') { - // If the ServiceWorker is not useable, we need to fallback to parse the DOM - // to inject math images, and replace some links with javascript calls - if (contentInjectionMode === 'jquery') { + // Convert links into javascript calls + $('#articleContent').contents().find('body').find('a').each(function() { + // Store current link's url + var url = $(this).attr("href"); + if (url === null || url === undefined) { + return; + } + var lowerCaseUrl = url.toLowerCase(); + var cssClass = $(this).attr("class"); - // Convert links into javascript calls - $('#articleContent').contents().find('body').find('a').each(function() { - // Store current link's url - var url = $(this).attr("href"); - if (url === null || url === undefined) { - return; + if (cssClass === "new") { + // It's a link to a missing article : display a message + $(this).on('click', function(e) { + alert("Missing article in Wikipedia"); + return false; + }); + } + else if (url.slice(0, 1) === "#") { + // It's an anchor link : do nothing + } + else if (url.substring(0, 4) === "http") { + // It's an external link : open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexpOtherLanguage)) { + // It's a link to another language : change the URL to the online version of wikipedia + // The regular expression extracts $1 as the language, and $2 as the title name + var onlineWikipediaUrl = url.replace(regexpOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); + $(this).attr("href", onlineWikipediaUrl); + // Open in a new tab + $(this).attr("target", "_blank"); + } + else if (url.match(regexpImageLink) + && (util.endsWith(lowerCaseUrl, ".png") + || util.endsWith(lowerCaseUrl, ".svg") + || util.endsWith(lowerCaseUrl, ".jpg") + || util.endsWith(lowerCaseUrl, ".jpeg"))) { + // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab + var onlineWikipediaUrl = url.replace(regexpImageLink, "https://" + selectedArchive._language + ".wikipedia.org/wiki/File:$1"); + $(this).attr("href", onlineWikipediaUrl); + $(this).attr("target", "_blank"); + } + else { + // It's a link to another article + // Add an onclick event to go to this article + // instead of following the link + if (url.length>=2 && url.substring(0, 2) === "./") { + url = url.substring(2); } - var lowerCaseUrl = url.toLowerCase(); - var cssClass = $(this).attr("class"); + $(this).on('click', function(e) { + var titleName = decodeURIComponent(url); + pushBrowserHistoryState(titleName); + goToArticle(titleName); + return false; + }); + } + }); - if (cssClass === "new") { - // It's a link to a missing article : display a message - $(this).on('click', function(e) { - alert("Missing article in Wikipedia"); - return false; - }); - } - else if (url.slice(0, 1) === "#") { - // It's an anchor link : do nothing - } - else if (url.substring(0, 4) === "http") { - // It's an external link : open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexpOtherLanguage)) { - // It's a link to another language : change the URL to the online version of wikipedia - // The regular expression extracts $1 as the language, and $2 as the title name - var onlineWikipediaUrl = url.replace(regexpOtherLanguage, "https://$1.wikipedia.org/wiki/$2"); - $(this).attr("href", onlineWikipediaUrl); - // Open in a new tab - $(this).attr("target", "_blank"); - } - else if (url.match(regexpImageLink) - && (util.endsWith(lowerCaseUrl, ".png") - || util.endsWith(lowerCaseUrl, ".svg") - || util.endsWith(lowerCaseUrl, ".jpg") - || util.endsWith(lowerCaseUrl, ".jpeg"))) { - // It's a link to a file of wikipedia : change the URL to the online version and open in a new tab - var onlineWikipediaUrl = url.replace(regexpImageLink, "https://" + selectedArchive._language + ".wikipedia.org/wiki/File:$1"); - $(this).attr("href", onlineWikipediaUrl); - $(this).attr("target", "_blank"); - } - else { - // It's a link to another article - // Add an onclick event to go to this article - // instead of following the link - if (url.length>=2 && url.substring(0, 2) === "./") { - url = url.substring(2); - } - $(this).on('click', function(e) { - var titleName = decodeURIComponent(url); - pushBrowserHistoryState(titleName); - goToArticle(titleName); - return false; - }); - } - }); - - // Load images - $('#articleContent').contents().find('body').find('img').each(function() { - var image = $(this); - var m = image.attr("src").match(regexpMathImageUrl); - if (m) { - // It's a math image (Evopedia archive) - selectedArchive.loadMathImage(m[1], function(data) { - image.attr("src", 'data:image/png;base64,' + data); - }); - } else { - // It's a standard image contained in the ZIM file - var imageMatch = image.attr("src").match(regexpImageUrl); - if (imageMatch) { - selectedArchive.getTitleByName(imageMatch[1]).then(function(title) { - selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - // TODO : add the complete MIME-type of the image (as read from the ZIM file) - image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content)); - }); - }).fail(function () { - console.error("could not find title for image:" + imageMatch[1]); - }); - } - } - }); - - // Load CSS content - $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { - var link = $(this); - var hrefMatch = link.attr("href").match(regexpMetadataUrl); - if (hrefMatch) { - // It's a CSS file contained in the ZIM file - selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { + // Load images + $('#articleContent').contents().find('body').find('img').each(function() { + var image = $(this); + var m = image.attr("src").match(regexpMathImageUrl); + if (m) { + // It's a math image (Evopedia archive) + selectedArchive.loadMathImage(m[1], function(data) { + image.attr("src", 'data:image/png;base64,' + data); + }); + } else { + // It's a standard image contained in the ZIM file + var imageMatch = image.attr("src").match(regexpImageUrl); + if (imageMatch) { + selectedArchive.getTitleByName(imageMatch[1]).then(function(title) { selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + // TODO : add the complete MIME-type of the image (as read from the ZIM file) + image.attr("src", 'data:image;base64,' + util.uint8ArrayToBase64(content)); }); }).fail(function () { - console.error("could not find title for CSS : " + hrefMatch[1]); + console.error("could not find title for image:" + imageMatch[1]); }); } - }); + } + }); - // Load Javascript content - $('#articleContent').contents().find('body').find('script').each(function() { - var script = $(this); - var srcMatch = script.attr("src").match(regexpMetadataUrl); - // TODO check that the type of the script is text/javascript or application/javascript - if (srcMatch) { - // It's a Javascript file contained in the ZIM file - selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { - selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); - }); - }).fail(function () { - console.error("could not find title for javascript : " + srcMatch[1]); + // Load CSS content + $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { + var link = $(this); + var hrefMatch = link.attr("href").match(regexpMetadataUrl); + if (hrefMatch) { + // It's a CSS file contained in the ZIM file + selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); }); - } - }); + }).fail(function () { + console.error("could not find title for CSS : " + hrefMatch[1]); + }); + } + }); - } + // Load Javascript content + $('#articleContent').contents().find('body').find('script').each(function() { + var script = $(this); + var srcMatch = script.attr("src").match(regexpMetadataUrl); + // TODO check that the type of the script is text/javascript or application/javascript + if (srcMatch) { + // It's a Javascript file contained in the ZIM file + selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { + selectedArchive.readBinaryFile(title, function (readableTitleName, content) { + script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + }); + }).fail(function () { + console.error("could not find title for javascript : " + srcMatch[1]); + }); + } + }); - }); + } } /** diff --git a/www/js/lib/zimArchive.js b/www/js/lib/zimArchive.js index ae1f2b0e..20050a19 100644 --- a/www/js/lib/zimArchive.js +++ b/www/js/lib/zimArchive.js @@ -200,6 +200,8 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'], callback(title.name(), data); }); }; + + var regexpTitleNameWithNamespaceA = /^A\//; /** * Searches a title (article / page) by name. @@ -208,6 +210,10 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'], */ ZIMArchive.prototype.getTitleByName = function(titleName) { var that = this; + // If the namespace is not mentioned, we have to add it + if (!regexpTitleNameWithNamespaceA.test(titleName)) { + titleName= "A/" + titleName; + } return util.binarySearch(0, this._file.articleCount, function(i) { return that._file.dirEntryByUrlIndex(i).then(function(dirEntry) { var url = dirEntry.namespace + "/" + dirEntry.url; From 6b9f70f300d536760132707519ad094d84e00740 Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 19:03:48 +0100 Subject: [PATCH 7/9] Fix for images, to reflect the need of the namespace to read in the ZIM backend. --- service-worker.js | 13 ++++++++----- tests/tests.js | 2 +- www/js/lib/zimArchive.js | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/service-worker.js b/service-worker.js index f41438d6..095f9885 100644 --- a/service-worker.js +++ b/service-worker.js @@ -90,6 +90,7 @@ function(util) { event.respondWith(new Promise(function(resolve, reject) { var nameSpace; var titleName; + var titleNameWithNameSpace; var contentType; if (regexpContentUrlWithoutNamespace.test(event.request.url)) { // When the request URL is in the same folder, @@ -140,12 +141,14 @@ function(util) { resolve(httpResponse); return; } + + titleNameWithNameSpace = nameSpace + '/' + titleName; // Let's instanciate a new messageChannel, to allow app.s to give us the content var messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function(event) { if (event.data.action === 'giveContent') { - console.log('content message received for ' + titleName, event.data); + console.log('content message received for ' + titleNameWithNameSpace, event.data); var responseInit = { status: 200, statusText: 'OK', @@ -156,16 +159,16 @@ function(util) { var httpResponse = new Response(event.data.content, responseInit); - console.log('ServiceWorker responding to the HTTP request for ' + titleName + ' (size=' + event.data.content.length + ' octets)' , httpResponse); + console.log('ServiceWorker responding to the HTTP request for ' + titleNameWithNameSpace + ' (size=' + event.data.content.length + ' octets)' , httpResponse); resolve(httpResponse); } else { - console.log('Invalid message received from app.js for ' + titleName, event.data); + console.log('Invalid message received from app.js for ' + titleNameWithNameSpace, event.data); reject(event.data); } }; - console.log('Eventlistener added to listen for an answer to ' + titleName); - outgoingMessagePort.postMessage({'action': 'askForContent', 'titleName': titleName}, [messageChannel.port2]); + console.log('Eventlistener added to listen for an answer to ' + titleNameWithNameSpace); + outgoingMessagePort.postMessage({'action': 'askForContent', 'titleName': titleNameWithNameSpace}, [messageChannel.port2]); console.log('Message sent to app.js through outgoingMessagePort'); })); } diff --git a/tests/tests.js b/tests/tests.js index 09ecf32d..dfcb8b3e 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -531,7 +531,7 @@ define(['jquery', 'title', 'archive', 'zimArchive', 'zimDirEntry', 'util', 'geom }); }); }); - asyncTest("Image 's/style.css' can be loaded", function() { + asyncTest("Stylesheet 's/style.css' can be loaded", function() { expect(4); localZimArchive.getTitleByName("-/s/style.css").then(function(title) { ok(title !== null, "Title found"); diff --git a/www/js/lib/zimArchive.js b/www/js/lib/zimArchive.js index 20050a19..1fedb8c2 100644 --- a/www/js/lib/zimArchive.js +++ b/www/js/lib/zimArchive.js @@ -201,7 +201,7 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'], }); }; - var regexpTitleNameWithNamespaceA = /^A\//; + var regexpTitleNameWithoutNameSpace = /^[^\/]+$/; /** * Searches a title (article / page) by name. @@ -210,8 +210,8 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'], */ ZIMArchive.prototype.getTitleByName = function(titleName) { var that = this; - // If the namespace is not mentioned, we have to add it - if (!regexpTitleNameWithNamespaceA.test(titleName)) { + // If no namespace is mentioned, it's an article, and we have to add it + if (regexpTitleNameWithoutNameSpace.test(titleName)) { titleName= "A/" + titleName; } return util.binarySearch(0, this._file.articleCount, function(i) { From d8678e3a83bf47b71f4edeed3eed9b934b8869a4 Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 20:18:16 +0100 Subject: [PATCH 8/9] Enable CSS stylesheets read from the backend. #115 It works both in jQuery and ServiceWorker modes. But, for now, in jQuery mode, it freezes the UI on Firefox OS. The javascript content is still not available for now. --- service-worker.js | 33 ++++++++++++++++++--------------- www/js/app.js | 17 +++++++++++------ www/js/lib/util.js | 22 ++++++++++++++++++---- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/service-worker.js b/service-worker.js index 095f9885..998af580 100644 --- a/service-worker.js +++ b/service-worker.js @@ -123,25 +123,28 @@ function(util) { console.log("It's a layout dependency : " + titleName); if (regexpJS.test(titleName)) { contentType = 'text/javascript'; + var responseInit = { + status: 200, + statusText: 'OK', + headers: { + 'Content-Type': contentType + } + }; + + var httpResponse = new Response(';', responseInit); + + // TODO : temporary before the backend actually sends a proper content + resolve(httpResponse); + return; } else if (regexpCSS.test(titleName)) { - contentType = 'image/css'; + contentType = 'text/css'; } - var responseInit = { - status: 200, - statusText: 'OK', - headers: { - 'Content-Type': contentType - } - }; - - var httpResponse = new Response(';', responseInit); - - // TODO : temporary before the backend actually sends a proper content - resolve(httpResponse); - return; } + // We need to remove the potential parameters in the URL + titleName = util.removeUrlParameters(titleName); + titleNameWithNameSpace = nameSpace + '/' + titleName; // Let's instanciate a new messageChannel, to allow app.s to give us the content @@ -156,7 +159,7 @@ function(util) { 'Content-Type': contentType } }; - + var httpResponse = new Response(event.data.content, responseInit); console.log('ServiceWorker responding to the HTTP request for ' + titleNameWithNameSpace + ' (size=' + event.data.content.length + ' octets)' , httpResponse); diff --git a/www/js/app.js b/www/js/app.js index d38e9ca1..5e352193 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1000,14 +1000,16 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction }); // Load CSS content - $('#articleContent').contents().find('body').find('link[rel=stylesheet]').each(function() { + $('#articleContent').contents().find('link[rel=stylesheet]').each(function() { var link = $(this); var hrefMatch = link.attr("href").match(regexpMetadataUrl); if (hrefMatch) { // It's a CSS file contained in the ZIM file - selectedArchive.getTitleByName(hrefMatch[1]).then(function(title) { + var titleName = util.removeUrlParameters(hrefMatch[1]); + selectedArchive.getTitleByName(titleName).then(function(title) { selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - link.attr("href", 'data:text/css;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + var cssContent = encodeURIComponent(util.uintToString(content)); + link.attr("href", 'data:text/css;charset=UTF-8,' + cssContent); }); }).fail(function () { console.error("could not find title for CSS : " + hrefMatch[1]); @@ -1016,15 +1018,18 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction }); // Load Javascript content - $('#articleContent').contents().find('body').find('script').each(function() { + $('#articleContent').contents().find('script').each(function() { var script = $(this); var srcMatch = script.attr("src").match(regexpMetadataUrl); // TODO check that the type of the script is text/javascript or application/javascript if (srcMatch) { // It's a Javascript file contained in the ZIM file - selectedArchive.getTitleByName(srcMatch[1]).then(function(title) { + var titleName = util.removeUrlParameters(srcMatch[1]); + selectedArchive.getTitleByName(titleName).then(function(title) { selectedArchive.readBinaryFile(title, function (readableTitleName, content) { - script.attr("src", 'data:text/javascript;charset=UTF-8,' + encodeURIComponent(util.uintToString(content))); + // TODO : I have to disable javascript for now + // var jsContent = encodeURIComponent(util.uintToString(content)); + //script.attr("src", 'data:text/javascript;charset=UTF-8,' + jsContent); }); }).fail(function () { console.error("could not find title for javascript : " + srcMatch[1]); diff --git a/www/js/lib/util.js b/www/js/lib/util.js index ff1bf9d8..7fbfb511 100644 --- a/www/js/lib/util.js +++ b/www/js/lib/util.js @@ -195,14 +195,27 @@ define(['q'], function(q) { /** * Converts a UInt Array to a UTF-8 encoded string + * source : http://michael-rushanan.blogspot.de/2014/03/javascript-uint8array-hacks-and-cheat.html * * @param {UIntArray} uintArray * @returns {String} */ function uintToString(uintArray) { - var encodedString = String.fromCharCode.apply(null, uintArray), - decodedString = decodeURIComponent(escape(encodedString)); - return decodedString; + var s = ''; + for (var i = 0; i < uintArray.length; i++) { + s += String.fromCharCode(uintArray[i]); + } + return s; + } + + var regexpRemoveUrlParameters = new RegExp(/([^\?]+)\?.*$/); + + function removeUrlParameters(url) { + if (regexpRemoveUrlParameters.test(url)) { + return regexpRemoveUrlParameters.exec(url)[1]; + } else { + return url; + } } /** @@ -218,6 +231,7 @@ define(['q'], function(q) { readFileSlice : readFileSlice, binarySearch: binarySearch, b64toBlob: b64toBlob, - uintToString: uintToString + uintToString: uintToString, + removeUrlParameters: removeUrlParameters }; }); From bf69bbc0b81f1e64c4b26702538a86eb34ac331a Mon Sep 17 00:00:00 2001 From: mossroy Date: Thu, 7 Jan 2016 20:29:18 +0100 Subject: [PATCH 9/9] Minor improvement in code readability --- www/js/app.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 5e352193..fcc7aca8 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -338,14 +338,7 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction // We need to wait for the ServiceWorker to be activated // before sending the first init message - var serviceWorker; - if (reg.installing) { - serviceWorker = reg.installing; - } else if (reg.waiting) { - serviceWorker = reg.waiting; - } else if (reg.active) { - serviceWorker = reg.active; - } + var serviceWorker = reg.installing || reg.waiting || reg.active; serviceWorker.addEventListener('statechange', function(statechangeevent) { if (statechangeevent.target.state === 'activated') { console.log("try to post an init message to ServiceWorker");