From b5cfc74e1275b3aac189ec9b0dc2fcc9d2baeb4e Mon Sep 17 00:00:00 2001 From: Fred Chasen Date: Tue, 8 Nov 2016 16:05:47 +0100 Subject: [PATCH] Added resources, renamed unarchive -> archive, moved replacements to Book --- src/{unarchive.js => archive.js} | 28 ++--- src/book.js | 98 +++++++++--------- src/core.js | 49 ++------- src/rendition.js | 155 ---------------------------- src/request.js | 5 +- src/resources.js | 172 +++++++++++++++++++++++++++++++ src/section.js | 3 +- test/epub.js | 2 +- 8 files changed, 252 insertions(+), 260 deletions(-) rename src/{unarchive.js => archive.js} (83%) create mode 100644 src/resources.js diff --git a/src/unarchive.js b/src/archive.js similarity index 83% rename from src/unarchive.js rename to src/archive.js index ccefc0d..39ab0cf 100644 --- a/src/unarchive.js +++ b/src/archive.js @@ -1,15 +1,16 @@ var core = require('./core'); var request = require('./request'); var mime = require('../libs/mime/mime'); +var Path = require('./core').Path; -function Unarchive() { +function Archive() { this.checkRequirements(); this.urlCache = {}; } -Unarchive.prototype.checkRequirements = function(callback){ +Archive.prototype.checkRequirements = function(callback){ try { if (typeof JSZip === 'undefined') { JSZip = require('jszip'); @@ -20,25 +21,26 @@ Unarchive.prototype.checkRequirements = function(callback){ } }; -Unarchive.prototype.open = function(input, isBase64){ +Archive.prototype.open = function(input, isBase64){ return this.zip.loadAsync(input, {"base64": isBase64}); }; -Unarchive.prototype.openUrl = function(zipUrl, isBase64){ +Archive.prototype.openUrl = function(zipUrl, isBase64){ return request(zipUrl, "binary") .then(function(data){ return this.zip.loadAsync(data, {"base64": isBase64}); }.bind(this)); }; -Unarchive.prototype.request = function(url, type){ +Archive.prototype.request = function(url, type){ var deferred = new core.defer(); var response; var r; + var path = new Path(url); // If type isn't set, determine it from the file extension if(!type) { - type = core.extension(url); + type = path.extension; } if(type == 'blob'){ @@ -61,7 +63,7 @@ Unarchive.prototype.request = function(url, type){ return deferred.promise; }; -Unarchive.prototype.handleResponse = function(response, type){ +Archive.prototype.handleResponse = function(response, type){ var r; if(type == "json") { @@ -85,7 +87,7 @@ Unarchive.prototype.handleResponse = function(response, type){ return r; }; -Unarchive.prototype.getBlob = function(url, _mimeType){ +Archive.prototype.getBlob = function(url, _mimeType){ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash var entry = this.zip.file(decodededUrl); var mimeType; @@ -98,7 +100,7 @@ Unarchive.prototype.getBlob = function(url, _mimeType){ } }; -Unarchive.prototype.getText = function(url, encoding){ +Archive.prototype.getText = function(url, encoding){ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash var entry = this.zip.file(decodededUrl); @@ -109,7 +111,7 @@ Unarchive.prototype.getText = function(url, encoding){ } }; -Unarchive.prototype.getBase64 = function(url, _mimeType){ +Archive.prototype.getBase64 = function(url, _mimeType){ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash var entry = this.zip.file(decodededUrl); var mimeType; @@ -122,7 +124,7 @@ Unarchive.prototype.getBase64 = function(url, _mimeType){ } }; -Unarchive.prototype.createUrl = function(url, options){ +Archive.prototype.createUrl = function(url, options){ var deferred = new core.defer(); var _URL = window.URL || window.webkitURL || window.mozURL; var tempUrl; @@ -175,10 +177,10 @@ Unarchive.prototype.createUrl = function(url, options){ return deferred.promise; }; -Unarchive.prototype.revokeUrl = function(url){ +Archive.prototype.revokeUrl = function(url){ var _URL = window.URL || window.webkitURL || window.mozURL; var fromCache = this.urlCache[url]; if(fromCache) _URL.revokeObjectURL(fromCache); }; -module.exports = Unarchive; +module.exports = Archive; diff --git a/src/book.js b/src/book.js index 6b6d6e5..13c707d 100644 --- a/src/book.js +++ b/src/book.js @@ -9,8 +9,9 @@ var Parser = require('./parser'); var Container = require('./container'); var Packaging = require('./packaging'); var Navigation = require('./navigation'); +var Resources = require('./resources'); var Rendition = require('./rendition'); -var Unarchive = require('./unarchive'); +var Archive = require('./archive'); var request = require('./request'); var EpubCFI = require('./epubcfi'); @@ -31,7 +32,8 @@ function Book(url, options){ this.settings = core.extend(this.settings || {}, { requestMethod: this.requestMethod, requestCredentials: undefined, - encoding: undefined // optional to pass 'binary' or base64' for archived Epubs + encoding: undefined, // optional to pass 'binary' or base64' for archived Epubs + base64: true }); core.extend(this.settings, options); @@ -51,7 +53,8 @@ function Book(url, options){ metadata: new core.defer(), cover: new core.defer(), navigation: new core.defer(), - pageList: new core.defer() + pageList: new core.defer(), + resources: new core.defer() }; this.loaded = { @@ -60,7 +63,8 @@ function Book(url, options){ metadata: this.loading.metadata.promise, cover: this.loading.cover.promise, navigation: this.loading.navigation.promise, - pageList: this.loading.pageList.promise + pageList: this.loading.pageList.promise, + resources: this.loading.resources.promise }; // this.ready = RSVP.hash(this.loaded); @@ -72,7 +76,7 @@ function Book(url, options){ this.loaded.metadata, this.loaded.cover, this.loaded.navigation, - this.loaded.pageList ]); + this.loaded.resources ]); // Queue for methods used before opening @@ -128,22 +132,20 @@ Book.prototype.open = function(input, what){ if (type === "binary") { this.archived = true; + this.url = new Url("/", ""); opening = this.openEpub(input); } else if (type === "epub") { this.archived = true; + this.url = new Url("/", ""); opening = this.request(input, 'binary') - .then(function(epubData) { - return this.openEpub(epubData); - }.bind(this)); + .then(this.openEpub.bind(this)); } else if(type == "opf") { this.url = new Url(input); opening = this.openPackaging(input); } else { this.url = new Url(input); opening = this.openContainer(CONTAINER_PATH) - .then(function(packagePath) { - return this.openPackaging(packagePath); - }.bind(this)) + .then(this.openPackaging.bind(this)); } return opening; @@ -152,10 +154,10 @@ Book.prototype.open = function(input, what){ Book.prototype.openEpub = function(data, encoding){ return this.unarchive(data, encoding || this.settings.encoding) .then(function() { - return this.openContainer("/" + CONTAINER_PATH); + return this.openContainer(CONTAINER_PATH); }.bind(this)) .then(function(packagePath) { - return this.openPackaging("/" + packagePath); + return this.openPackaging(packagePath); }.bind(this)); }; @@ -163,7 +165,7 @@ Book.prototype.openContainer = function(url){ return this.load(url) .then(function(xml) { this.container = new Container(xml); - return this.container.packagePath; + return this.resolve(this.container.packagePath); }.bind(this)); }; @@ -180,9 +182,9 @@ Book.prototype.openPackaging = function(url){ Book.prototype.load = function (path) { var resolved; - if(this.unarchived) { + if(this.archived) { resolved = this.resolve(path); - return this.unarchived.request(resolved); + return this.archive.request(resolved); } else { resolved = this.resolve(path); return this.request(resolved, null, this.requestCredentials, this.requestHeaders); @@ -244,6 +246,12 @@ Book.prototype.unpack = function(opf){ this.spine.unpack(this.package, this.resolve.bind(this)); + this.resources = new Resources(this.package.manifest, { + archive: this.archive, + resolver: this.resolve.bind(this), + base64: this.settings.base64 + }); + this.loadNavigation(this.package).then(function(toc){ this.toc = toc; this.loading.navigation.resolve(this.toc); @@ -256,11 +264,20 @@ Book.prototype.unpack = function(opf){ this.loading.metadata.resolve(this.package.metadata); this.loading.spine.resolve(this.spine); this.loading.cover.resolve(this.cover); + this.loading.resources.resolve(this.resources); + this.isOpen = true; - // Resolve book opened promise - this.opening.resolve(this); + if(this.archived) { + this.replacements().then(function() { + this.opening.resolve(this); + }.bind(this)); + } else { + // Resolve book opened promise + this.opening.resolve(this); + } + }; Book.prototype.loadNavigation = function(opf){ @@ -311,34 +328,8 @@ Book.prototype.setRequestHeaders = function(_headers) { * Unarchive a zipped epub */ Book.prototype.unarchive = function(bookUrl, encoding){ - this.unarchived = new Unarchive(); - return this.unarchived.open(bookUrl, encoding); -}; - -/** - * Checks if url has a .epub or .zip extension, or is ArrayBuffer (of zip/epub) - */ -Book.prototype.isArchivedUrl = function(bookUrl){ - var extension; - - if (bookUrl instanceof ArrayBuffer) { - return true; - } - - // Reuse parsed url or create a new uri object - // if(typeof(bookUrl) === "object") { - // uri = bookUrl; - // } else { - // uri = core.uri(bookUrl); - // } - // uri = URI(bookUrl); - extension = core.extension(bookUrl); - - if(extension && (extension == "epub" || extension == "zip")){ - return true; - } - - return false; + this.archive = new Archive(); + return this.archive.open(bookUrl, encoding); }; /** @@ -347,8 +338,8 @@ Book.prototype.isArchivedUrl = function(bookUrl){ Book.prototype.coverUrl = function(){ var retrieved = this.loaded.cover. then(function(url) { - if(this.unarchived) { - return this.unarchived.createUrl(this.cover); + if(this.archived) { + return this.archive.createUrl(this.cover); }else{ return this.cover; } @@ -359,6 +350,17 @@ Book.prototype.coverUrl = function(){ return retrieved; }; +Book.prototype.replacements = function(){ + this.spine.hooks.serialize.register(function(output, section) { + section.output = this.resources.substitute(output, section.url); + }.bind(this)); + + return this.resources.replacements(). + then(function() { + return this.resources.replaceCss(); + }.bind(this)); +}; + /** * Find a DOM Range for a given CFI Range */ diff --git a/src/core.js b/src/core.js index d34d43a..b006e70 100644 --- a/src/core.js +++ b/src/core.js @@ -12,6 +12,7 @@ var requestAnimationFrame = (typeof window != 'undefined') ? (window.requestAnim */ function Url(urlString, baseString) { var absolute = (urlString.indexOf('://') > -1); + var pathname; this.href = urlString; this.protocol = ""; @@ -20,7 +21,7 @@ function Url(urlString, baseString) { this.search = ""; this.base = baseString || undefined; - if (!absolute && !baseString) { + if (!absolute && typeof(baseString) !== "string") { this.base = window && window.location.href; } @@ -32,12 +33,15 @@ function Url(urlString, baseString) { this.origin = this.Url.origin; this.fragment = this.Url.fragment; this.search = this.Url.search; + + pathname = this.Url.pathname; } catch (e) { // console.error(e); this.Url = undefined; + pathname = urlString; } - this.Path = new Path(this.Url.pathname); + this.Path = new Path(pathname); this.directory = this.Path.directory; this.filename = this.Path.filename; this.extension = this.Path.extension; @@ -96,6 +100,10 @@ Path.prototype.parse = function (what) { return path.parse(what); }; +Path.prototype.isAbsolute = function (what) { + return path.isAbsolute(what || this.path); +}; + Path.prototype.isDirectory = function (what) { return (what.charAt(what.length-1) === '/'); }; @@ -122,41 +130,6 @@ function assertPath(path) { } }; -function extension(_url) { - var url; - var pathname; - var ext; - - try { - url = new Url(url); - pathname = url.pathname; - } catch (e) { - pathname = _url; - } - - ext = path.extname(pathname); - if (ext) { - return ext.slice(1); - } - - return ''; -} - -function directory(_url) { - var url; - var pathname; - var ext; - - try { - url = new Url(url); - pathname = url.pathname; - } catch (e) { - pathname = _url; - } - - return path.dirname(pathname); -} - function isElement(obj) { return !!(obj && obj.nodeType == 1); }; @@ -630,8 +603,6 @@ function defer() { module.exports = { - 'extension' : extension, - 'directory' : directory, 'isElement': isElement, 'uuid': uuid, 'values': values, diff --git a/src/rendition.js b/src/rendition.js index 60f16e8..fcf5f72 100644 --- a/src/rendition.js +++ b/src/rendition.js @@ -58,11 +58,6 @@ function Rendition(book, options) { // this.starting = new core.defer(); // this.started = this.starting.promise; this.q.enqueue(this.start); - - if(this.book.archived) { - this.q.enqueue(this.replacements.bind(this)); - } - }; Rendition.prototype.setManager = function(manager) { @@ -400,156 +395,6 @@ Rendition.prototype.triggerSelectedEvent = function(cfirange){ this.emit("selected", cfirange); }; -Rendition.prototype.replacements = function(){ - // Wait for loading - // return this.q.enqueue(function () { - // Get thes books manifest - var manifest = this.book.package.manifest; - var manifestArray = Object.keys(manifest). - map(function (key){ - return manifest[key]; - }); - - // Exclude HTML - var items = manifestArray. - filter(function (item){ - if (item.type != "application/xhtml+xml" && - item.type != "text/html") { - return true; - } - }); - - // Only CSS - var css = items. - filter(function (item){ - if (item.type === "text/css") { - return true; - } - }); - - // Css Urls - var cssUrls = css.map(function(item) { - return item.href; - }); - - // All Assets Urls - var urls = items. - map(function(item) { - return item.href; - }.bind(this)); - - // Create blob urls for all the assets - var processing = urls. - map(function(url) { - // var absolute = new URL(url, this.book.baseUrl).toString(); - var absolute = this.book.resolve(url); - // Full url from archive base - return this.book.unarchived.createUrl(absolute, {"base64": this.settings.useBase64}); - }.bind(this)); - - var replacementUrls; - - // After all the urls are created - return Promise.all(processing) - .then(function(_replacementUrls) { - var replaced = []; - - replacementUrls = _replacementUrls; - - // Replace Asset Urls in the text of all css files - cssUrls.forEach(function(href) { - replaced.push(this.replaceCss(href, urls, replacementUrls)); - }.bind(this)); - - return Promise.all(replaced); - - }.bind(this)) - .then(function () { - // Replace Asset Urls in chapters - // by registering a hook after the sections contents has been serialized - this.book.spine.hooks.serialize.register(function(output, section) { - - this.replaceAssets(section, urls, replacementUrls); - - }.bind(this)); - - }.bind(this)) - .catch(function(reason){ - console.error(reason); - }); - // }.bind(this)); -}; - -Rendition.prototype.replaceCss = function(href, urls, replacementUrls){ - var newUrl; - var indexInUrls; - - // Find the absolute url of the css file - // var fileUri = URI(href); - // var absolute = fileUri.absoluteTo(this.book.baseUrl).toString(); - - if (path.isAbsolute(href)) { - return new Promise(function(resolve, reject){ - resolve(urls, replacementUrls); - }); - } - - var fileUri; - var absolute = this.book.resolve(href); - - // Get the text of the css file from the archive - var textResponse = this.book.unarchived.getText(absolute); - // Get asset links relative to css file - var relUrls = urls. - map(function(assetHref) { - var resolved = this.book.resolve(assetHref); - var relative = new Path(absolute).relative(resolved); - - return relative; - }.bind(this)); - - return textResponse.then(function (text) { - // Replacements in the css text - text = replace.substitute(text, relUrls, replacementUrls); - - // Get the new url - if (this.settings.useBase64) { - newUrl = core.createBase64Url(text, 'text/css'); - } else { - newUrl = core.createBlobUrl(text, 'text/css'); - } - - // switch the url in the replacementUrls - indexInUrls = urls.indexOf(href); - if (indexInUrls > -1) { - replacementUrls[indexInUrls] = newUrl; - } - - return new Promise(function(resolve, reject){ - resolve(urls, replacementUrls); - }); - - }.bind(this)); - -}; - -Rendition.prototype.replaceAssets = function(section, urls, replacementUrls){ - // var fileUri = URI(section.url); - var fileUri; - var absolute = section.url; - - // Get Urls relative to current sections - var relUrls = urls. - map(function(href) { - var resolved = this.book.resolve(href); - var relative = new Path(absolute).relative(resolved); - - return relative; - }.bind(this)); - - section.output = replace.substitute(section.output, relUrls, replacementUrls); -}; - Rendition.prototype.range = function(_cfi, ignoreClass){ var cfi = new EpubCFI(_cfi); var found = this.visible().filter(function (view) { diff --git a/src/request.js b/src/request.js index fb21c95..1dea92b 100644 --- a/src/request.js +++ b/src/request.js @@ -1,4 +1,5 @@ var core = require('./core'); +var Path = require('./core').Path; function request(url, type, withCredentials, headers) { var supportsURL = (typeof window != "undefined") ? window.URL : false; // TODO: fallback for url if window isn't defined @@ -40,9 +41,7 @@ function request(url, type, withCredentials, headers) { // If type isn't set, determine it from the file extension if(!type) { - // uri = new URI(url); - // type = uri.suffix(); - type = core.extension(url); + type = new Path(url).extension; } if(type == 'blob'){ diff --git a/src/resources.js b/src/resources.js new file mode 100644 index 0000000..3fbf242 --- /dev/null +++ b/src/resources.js @@ -0,0 +1,172 @@ +var replace = require('./replacements'); +var core = require('./core'); +var Path = require('./core').Path; +var path = require('path'); + +function Resources(manifest, options) { + this.settings = { + base64: (options && options.base64) || true, + archive: (options && options.archive), + resolver: (options && options.resolver) + }; + this.manifest = manifest; + this.resources = Object.keys(manifest). + map(function (key){ + return manifest[key]; + }); + + this.replacementUrls = undefined; + + this.split(); + this.urls(); +} + +Resources.prototype.split = function(){ + + // HTML + this.html = this.resources. + filter(function (item){ + if (item.type === "application/xhtml+xml" || + item.type === "text/html") { + return true; + } + }); + + // Exclude HTML + this.assets = this.resources. + filter(function (item){ + if (item.type !== "application/xhtml+xml" && + item.type !== "text/html") { + return true; + } + }); + + // Only CSS + this.css = this.resources. + filter(function (item){ + if (item.type === "text/css") { + return true; + } + }); +}; + +Resources.prototype.urls = function(){ + + // All Assets Urls + this.urls = this.assets. + map(function(item) { + return item.href; + }.bind(this)); + + // Css Urls + this.cssUrls = this.css.map(function(item) { + return item.href; + }); + +}; + +/** + * Create blob urls for all the assets + * @param {Archive} archive + * @param {resolver} resolver Url resolver + * @return {Promise} returns replacement urls + */ +Resources.prototype.replacements = function(archive, resolver){ + archive = archive || this.settings.archive; + resolver = resolver || this.settings.resolver; + var replacements = this.urls. + map(function(url) { + var absolute = resolver(url); + + return archive.createUrl(absolute, {"base64": this.settings.base64}); + }.bind(this)) + + return Promise.all(replacements) + .then(function(replacementUrls) { + this.replacementUrls = replacementUrls; + return replacementUrls; + }.bind(this)); +}; + +Resources.prototype.replaceCss = function(archive, resolver){ + var replaced = []; + archive = archive || this.settings.archive; + resolver = resolver || this.settings.resolver; + this.cssUrls.forEach(function(href) { + var replacment = this.createCssFile(href, archive, resolver) + .then(function (replacementUrl) { + // switch the url in the replacementUrls + var indexInUrls = this.urls.indexOf(href); + if (indexInUrls > -1) { + this.replacementUrls[indexInUrls] = replacementUrl; + } + }.bind(this)); + + replaced.push(replacment); + }.bind(this)); + return Promise.all(replaced); +}; + +Resources.prototype.createCssFile = function(href, archive, resolver){ + var newUrl; + var indexInUrls; + archive = archive || this.settings.archive; + resolver = resolver || this.settings.resolver; + + if (path.isAbsolute(href)) { + return new Promise(function(resolve, reject){ + resolve(urls, replacementUrls); + }); + } + + var absolute = resolver(href); + + // Get the text of the css file from the archive + var textResponse = archive.getText(absolute); + // Get asset links relative to css file + var relUrls = this.urls.map(function(assetHref) { + var resolved = resolver(assetHref); + var relative = new Path(absolute).relative(resolved); + + return relative; + }.bind(this)); + + return textResponse.then(function (text) { + // Replacements in the css text + text = replace.substitute(text, relUrls, this.replacementUrls); + + // Get the new url + if (this.settings.base64) { + newUrl = core.createBase64Url(text, 'text/css'); + } else { + newUrl = core.createBlobUrl(text, 'text/css'); + } + + return newUrl; + }.bind(this)); + +}; + +Resources.prototype.relativeTo = function(absolute, resolver){ + resolver = resolver || this.settings.resolver; + + // Get Urls relative to current sections + return this.urls. + map(function(href) { + var resolved = resolver(href); + var relative = new Path(absolute).relative(resolved); + return relative; + }.bind(this)); +}; + +Resources.prototype.substitute = function(content, url) { + var relUrls; + if (url) { + relUrls = this.relativeTo(url); + } else { + relUrls = this.urls; + } + return replace.substitute(content, relUrls, this.replacementUrls); +}; + +module.exports = Resources; diff --git a/src/section.js b/src/section.js index 39fa38c..21cf09e 100644 --- a/src/section.js +++ b/src/section.js @@ -1,6 +1,7 @@ var core = require('./core'); var EpubCFI = require('./epubcfi'); var Hook = require('./hook'); +var Url = require('./core').Url; function Section(item, hooks){ this.idref = item.idref; @@ -36,7 +37,7 @@ Section.prototype.load = function(_request){ request(this.url) .then(function(xml){ var base; - var directory = core.directory(this.url); + var directory = new Url(this.url).directory; this.document = xml; this.contents = xml.documentElement; diff --git a/test/epub.js b/test/epub.js index ed5cb37..e063ee5 100644 --- a/test/epub.js +++ b/test/epub.js @@ -45,7 +45,7 @@ describe('ePub', function() { return book.opened.then(function(){ assert.equal( book.isOpen, true, "book is opened" ); - assert( book.unarchived, "book is unarchived" ); + assert( book.archive, "book is unarchived" ); }); });