Merge pull request #142 from mossroy/jquery-fallback

JQuery fallback and injection of stylesheets
This commit is contained in:
Mossroy 2016-01-07 20:35:57 +01:00
commit 21ce7b14d3
6 changed files with 355 additions and 155 deletions

View File

@ -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) {
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');
}
});
@ -91,21 +74,35 @@ function(util) {
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$/);
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)) {
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 titleNameWithNameSpace;
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
@ -126,17 +123,6 @@ function(util) {
console.log("It's a layout dependency : " + titleName);
if (regexpJS.test(titleName)) {
contentType = 'text/javascript';
}
else if (regexpCSS.test(titleName)) {
contentType = 'image/css';
}
}
// 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);
var responseInit = {
status: 200,
statusText: 'OK',
@ -145,23 +131,51 @@ function(util) {
}
};
var httpResponse = new Response(';', responseInit);
// TODO : temporary before the backend actually sends a proper content
resolve(httpResponse);
return;
}
else if (regexpCSS.test(titleName)) {
contentType = 'text/css';
}
}
// 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
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.action === 'giveContent') {
console.log('content message received for ' + titleNameWithNameSpace, event.data);
var responseInit = {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': contentType
}
};
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');
}));
}
// 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.
});
}
});

View File

@ -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");

View File

@ -207,6 +207,12 @@
<br /> Please select the archive you want to use : <select id="archiveList" class="form-control"></select>
<br /> Click <a id="btnRescanDeviceStorage">here</a> to rescan your SD Cards and internal memory
</div>
<div id="contentInjectionModeDiv">
Content injection mode : <br/>
<input type="radio" name="contentInjectionMode" value="jquery" id="jQueryModeRadio" checked><label for="jQueryModeRadio">JQuery</label>
<br>
<input type="radio" name="contentInjectionMode" value="serviceworker" id="serviceworkerModeRadio"><label for="serviceworkerModeRadio">ServiceWorker</label>
</div>
<div id="serviceWorkerStatus"></div>
<div id="messageChannelStatus"></div>
<br />

View File

@ -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,137 @@ 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 = 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");
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]').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);
}
/**
* 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 && 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);
}
else {
setContentInjectionMode('jquery');
}
var serviceWorkerRegistration = null;
@ -305,42 +419,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.<StorageFirefoxOS|StoragePhoneGap>
@ -532,8 +614,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 +634,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();
}
}
/**
@ -701,13 +787,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),
@ -745,13 +831,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.
@ -797,6 +877,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,
@ -810,25 +892,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) {
@ -837,17 +900,14 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
var currentPath = regexpPath.exec(currentHref)[1];
$('#articleContent').contents().find('head').append("<link rel='stylesheet' type='text/css' href='" + currentPath + "css/mediawiki-main.css' id='mediawiki-stylesheet' />");
}
else {
// TODO temporary test to inject CSS inside the iframe
$('#articleContent').contents().find('head').empty();
$('#articleContent').contents().find('head').append("<link rel='stylesheet' href='data:text/css;charset=UTF-8," + encodeURIComponent("body {background: #E9E9E9;}") + "' />");
}
// 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() {
@ -906,18 +966,71 @@ define(['jquery', 'abstractBackend', 'util', 'cookies','geometry','osabstraction
});
}
});
}
// Load math images
$('#articleContent').contents().find('body').find('img').each(function() {
var image = $(this);
var m = image.attr("src").match(regexpMathImageUrl);
if (m) {
selectedArchive.loadMathImage(m[1], function(data) {
image.attr("src", 'data:image/png;base64,' + data);
});
}
});
// 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('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
var titleName = util.removeUrlParameters(hrefMatch[1]);
selectedArchive.getTitleByName(titleName).then(function(title) {
selectedArchive.readBinaryFile(title, function (readableTitleName, 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]);
});
}
});
// Load Javascript content
$('#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
var titleName = util.removeUrlParameters(srcMatch[1]);
selectedArchive.getTitleByName(titleName).then(function(title) {
selectedArchive.readBinaryFile(title, function (readableTitleName, 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]);
});
}
});
}
}
/**

View File

@ -159,6 +159,64 @@ 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
* source : http://michael-rushanan.blogspot.de/2014/03/javascript-uint8array-hacks-and-cheat.html
*
* @param {UIntArray} uintArray
* @returns {String}
*/
function uintToString(uintArray) {
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;
}
}
/**
* Functions and classes exposed by this module
@ -171,6 +229,9 @@ define(['q'], function(q) {
uint8ArrayToHex : uint8ArrayToHex,
uint8ArrayToBase64 : uint8ArrayToBase64,
readFileSlice : readFileSlice,
binarySearch: binarySearch
binarySearch: binarySearch,
b64toBlob: b64toBlob,
uintToString: uintToString,
removeUrlParameters: removeUrlParameters
};
});

View File

@ -200,6 +200,8 @@ define(['zimfile', 'zimDirEntry', 'util', 'utf8'],
callback(title.name(), data);
});
};
var regexpTitleNameWithoutNameSpace = /^[^\/]+$/;
/**
* 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 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) {
return that._file.dirEntryByUrlIndex(i).then(function(dirEntry) {
var url = dirEntry.namespace + "/" + dirEntry.url;