define('models/fetch_base',['backbone'], function (Backbone) { var EpubFetchBase = Backbone.Model.extend({ _handleError: function (err) { console.log(err); console.trace(); }, parseXml: function (xmlString) { return this.parseMarkup(xmlString, 'text/xml'); }, parseMarkup: function (markupString, contentType) { var parser = new window.DOMParser; var parsedDom = parser.parseFromString(markupString, contentType); return parsedDom; } }); return EpubFetchBase; }); define('models/discover_content_type',['require', 'module', 'jquery', 'backbone', 'URIjs/URI'], function (require, module, $, Backbone, URI) { console.log('discover_content_type module id: ' + module.id); var ContentTypeDiscovery = Backbone.Model.extend({ initialize: function (attributes) { }, identifyContentTypeFromFileName: function (contentUrl) { var contentUrlSuffix = URI(contentUrl).suffix(); var contentType = 'application/octet-stream'; if (typeof this.constructor.suffixContentTypeMap[contentUrlSuffix] !== 'undefined') { contentType = this.constructor.suffixContentTypeMap[contentUrlSuffix]; } return contentType; }, identifyContentType: function () { // TODO: Make the call asynchronous (which would require a callback and would probably make sense // when calling functions are also remodelled for async). var contentUrl = this.get('contentUrl'); var contentType = $.ajax({ type: "HEAD", url: contentUrl, async: false }).getResponseHeader('Content-Type'); if (contentType === null) { contentType = this.identifyContentTypeFromFileName(contentUrl); console.log('guessed contentType [' + contentType + '] from URI [' + contentUrl + ']. Configuring the web server to provide the content type is recommended.'); } return contentType; } }, { suffixContentTypeMap: { css: 'text/css', epub: 'application/epub+zip', gif: 'image/gif', html: 'text/html', jpg: 'image/jpeg', jpeg: 'image/jpeg', ncx: 'application/x-dtbncx+xml', opf: 'application/oebps-package+xml', png: 'image/png', svg: 'image/svg+xml', xhtml: 'application/xhtml+xml' } }); return ContentTypeDiscovery; }); define('models/plain_fetcher',['require', 'module', 'jquery', 'URIjs/URI', './fetch_base'], function (require, module, $, URI, EpubFetchBase) { console.log('plain_fetcher module id: ' + module.id); var PlainExplodedFetcher = EpubFetchBase.extend({ initialize: function (attributes) { }, // Plain exploded EPUB packages are exploded by definition: isExploded: function () { return true; }, resolveURI: function (epubResourceURI) { // Make absolute to the package document path var epubResourceRelURI = new URI(epubResourceURI); var epubResourceAbsURI = epubResourceRelURI.absoluteTo(this.get('baseUrl')); return epubResourceAbsURI.toString(); }, fetchFileContentsText: function (fileUrl, fetchCallback, onerror) { var thisFetcher = this; if (typeof fileUrl === 'undefined') { throw 'Fetched file URL is undefined!'; } $.ajax({ url: fileUrl, dataType: 'text', success: function (result) { fetchCallback(result); }, error: function (xhr, status, errorThrown) { console.log('Error when AJAX fetching ' + fullUrl); console.log(status); console.log(errorThrown); onerror(errorThrown); } }); }, relativeToPackageFetchFileContents: function (relativeToPackagePath, fetchMode, fetchCallback, onerror) { // Not translating relativeToPackagePath, as with exploded EPUB all the URLs are relative // to the current page context and are good to go verbatim for fetching: this.fetchFileContentsText(relativeToPackagePath, fetchCallback, onerror); }, getPackageDom: function (callback) { console.log('getting package DOM'); var thisFetcher = this; var baseUrl = thisFetcher.get('baseUrl'); console.log('baseUrl: ' + baseUrl); thisFetcher.fetchFileContentsText(baseUrl, function (packageXml) { var packageDom = thisFetcher.parseXml(packageXml); callback(packageDom); }, this._handleError); } }); return PlainExplodedFetcher; }); define('models/zip_fetcher',['require', 'module', 'jquery', 'URIjs/URI', './fetch_base'], function (require, module, $, URI, EpubFetchBase) { console.log('zip_fetcher module id: ' + module.id); var ZipFetcher = EpubFetchBase.extend({ defaults: { 'checkCrc32': false }, initialize: function (attributes) { }, // Description: perform a function with an initialized zip filesystem, making sure that such filesystem is initialized. // Note that due to a race condition, more than one zip filesystem may be instantiated. // However, the last one to be set on the model object will prevail and others would be garbage collected later. _withZipFsPerform: function (callback, onerror) { var thisFetcher = this; if (thisFetcher.has('_zipFs')) { var zipFs = thisFetcher.get('_zipFs'); callback(zipFs); } else { var zipUrl = thisFetcher.get('baseUrl'); var libDir = thisFetcher.get('libDir'); console.log('zip.workerScriptsPath = ' + libDir); zip.workerScriptsPath = libDir; var zipFs = new zip.fs.FS(); zipFs.importHttpContent(zipUrl, true, function () { thisFetcher.set('_zipFs', zipFs); callback(zipFs); }, onerror) } }, _identifyContentTypeFromFileName: function (fileUri) { return this.get('_contentTypeDiscovery').identifyContentTypeFromFileName(fileUri); }, // Zipped EPUB packages are not exploded by definition: isExploded: function () { return false; }, resolveURI: function (epubResourceURI) { return epubResourceURI; }, fetchFileContents: function (relativePath, readCallback, onerror) { var thisFetcher = this; if (typeof relativePath === 'undefined') { throw 'Fetched file relative path is undefined!'; } this._withZipFsPerform(function (zipFs) { var entry = zipFs.find(relativePath); if (typeof entry === 'undefined' || entry === null) { onerror(new Error('Entry ' + relativePath + ' not found in zip ' + thisFetcher.get('baseUrl'))); } else { if (entry.directory) { onerror(new Error('Entry ' + relativePath + ' is a directory while a file has been expected')); } else { readCallback(entry); } } }, thisFetcher._handleError); }, fetchFileContentsText: function (relativePath, fetchCallback, onerror) { var thisFetcher = this; thisFetcher.fetchFileContents(relativePath, function (entry) { entry.getText(fetchCallback, undefined, thisFetcher.get('checkCrc32')); }, onerror) }, fetchFileContentsData64Uri: function (relativePath, fetchCallback, onerror) { var thisFetcher = this; thisFetcher.fetchFileContents(relativePath, function (entry) { entry.getData64URI(thisFetcher._identifyContentTypeFromFileName(relativePath), fetchCallback, undefined, thisFetcher.get('checkCrc32')); }, onerror) }, fetchFileContentsBlob: function (relativePath, fetchCallback, onerror) { var thisFetcher = this; thisFetcher.fetchFileContents(relativePath, function (entry) { entry.getBlob(thisFetcher._identifyContentTypeFromFileName(relativePath), fetchCallback, undefined, thisFetcher.get('checkCrc32')); }, onerror) }, relativeToPackageFetchFileContents: function (relativeToPackagePath, fetchMode, fetchCallback, onerror) { var thisFetcher = this; var packageFullPath = thisFetcher.get('_packageFullPath'); console.log('Have got _packageFullPath ' + packageFullPath); console.log('packageFullPath: ' + packageFullPath); console.log('relativePath: ' + relativeToPackagePath); var pathRelativeToPackage = decodeURIComponent(new URI(relativeToPackagePath).absoluteTo(packageFullPath).toString()); console.log('pathRelativeToPackage: ' + pathRelativeToPackage); var fetchFunction = thisFetcher.fetchFileContentsText; if (fetchMode === 'blob') { fetchFunction = thisFetcher.fetchFileContentsBlob; } else if (fetchMode === 'data64uri') { fetchFunction = thisFetcher.fetchFileContentsData64Uri; } fetchFunction.call(thisFetcher, pathRelativeToPackage, fetchCallback, onerror); }, getFileContentsFromPackage: function (fileRelativePath, callback) { var thisFetcher = this; thisFetcher.fetchFileContentsText(fileRelativePath, function (fileContents) { callback(fileContents); }, thisFetcher._handleError); }, getContainerXml: function (callback) { var fileRelativePath = 'META-INF/container.xml'; this.getFileContentsFromPackage(fileRelativePath, callback); }, getXmlFileDom: function (xmlFileRelativePath, callback) { var thisFetcher = this; thisFetcher.getFileContentsFromPackage(xmlFileRelativePath, function (xmlFileContents) { var fileDom = thisFetcher.parseXml(xmlFileContents); callback(fileDom); }); }, getPackageFullPath: function (callback) { var thisFetcher = this; thisFetcher.getXmlFileDom('META-INF/container.xml', function (containerXmlDom) { thisFetcher.getRootFile(containerXmlDom, callback); }); }, getRootFile: function (containerXmlDom, callback) { var rootFile = $('rootfile', containerXmlDom); var packageFullPath = rootFile.attr('full-path'); console.log('packageFullPath: ' + packageFullPath); callback(packageFullPath); }, getPackageDom: function (callback) { var thisFetcher = this; if (thisFetcher.has('_packageDom')) { callback(thisFetcher.get('_packageDom')); } else { // TODO: use jQuery's Deferred // Register all callbacks interested in initialized packageDom, launch its instantiation only once // and broadcast to all callbacks registered during the initialization once it's done: if (thisFetcher.has('_packageDomInitializationSubscription')) { thisFetcher.get('_packageDomInitializationSubscription').push(callback); } else { thisFetcher.set('_packageDomInitializationSubscription', [callback]); thisFetcher.getPackageFullPath(function (packageFullPath) { thisFetcher.set('_packageFullPath', packageFullPath); console.log('Have set _packageFullPath' + packageFullPath); thisFetcher.getXmlFileDom(packageFullPath, function (packageDom) { thisFetcher.set('_packageDom', packageDom); var initializationSubscriptions = thisFetcher.get('_packageDomInitializationSubscription'); thisFetcher.unset('_packageDomInitializationSubscription'); initializationSubscriptions.forEach(function (subscriberCallback) { subscriberCallback(packageDom); }); }) }); } } } }); return ZipFetcher; }); define('models/resource_resolver',['require', 'module', 'jquery', 'URIjs/URI', './fetch_base'], function (require, module, $, URI, EpubFetchBase) { console.log('resource_resolver module id: ' + module.id); var ResourceResolver = EpubFetchBase.extend({ initialize: function (attributes) { }, _resolveResourceElements: function (elemName, refAttr, contentDocumentDom, contentDocumentURI, resolutionDeferreds, onerror) { var thisResolver = this; var fetcher = thisResolver.get('_resourceFetcher'); var resolvedElems = $(elemName + '[' + refAttr + ']', contentDocumentDom); resolvedElems.each(function (index, resolvedElem) { var refAttrVal = $(resolvedElem).attr(refAttr); var refAttrUri = new URI(refAttrVal); if (refAttrUri.scheme() === '') { // Relative URI, fetch from packed EPUB archive: var resolutionDeferred = $.Deferred(); resolutionDeferreds.push(resolutionDeferred); var uriRelativeToZipRoot = refAttrUri.absoluteTo(contentDocumentURI).toString(); fetcher.relativeToPackageFetchFileContents(uriRelativeToZipRoot, 'blob', function (resourceData) { $(resolvedElem).attr(refAttr, window.URL.createObjectURL(resourceData)); resolutionDeferred.resolve(); }, onerror); } }); }, resolveInternalPackageResources: function (contentDocumentURI, contentDocumentType, contentDocumentText, resolvedDocumentCallback, onerror) { var thisResolver = this; var contentDocumentDom = this.parseMarkup(contentDocumentText, contentDocumentType); var resolutionDeferreds = []; thisResolver._resolveResourceElements('img', 'src', contentDocumentDom, contentDocumentURI, resolutionDeferreds, onerror); thisResolver._resolveResourceElements('link', 'href', contentDocumentDom, contentDocumentURI, resolutionDeferreds, onerror); $.when.apply($, resolutionDeferreds).done(function () { resolvedDocumentCallback(contentDocumentDom); }); } }); return ResourceResolver; }); define('models/package_fetcher',['require', 'module', './fetch_base', './discover_content_type', './plain_fetcher', './zip_fetcher', './resource_resolver'], function (require, module, EpubFetchBase, ContentTypeDiscovery, PlainExplodedFetcher, ZipFetcher, ResourceResolver) { console.log('package_fetcher module id: ' + module.id); var PackageFetcher = EpubFetchBase.extend({ initialize: function (attributes) { var contentTypeDiscovery = new ContentTypeDiscovery({'contentUrl': this.get('packageDocumentURL')}); this.set('_contentTypeDiscovery', contentTypeDiscovery); this._setupPackageContentType(); this._setupResourceFetcher(); }, _setupPackageContentType: function () { this.set('_packageContentType', this.get('_contentTypeDiscovery').identifyContentType()); }, _getPackageReadStrategy: function () { var readStrategy = 'exploded'; var packageContentType = this.getPackageContentType(); if (packageContentType in this.constructor.contentTypePackageReadStrategyMap) { readStrategy = this.constructor.contentTypePackageReadStrategyMap[packageContentType] } return readStrategy; }, _setupResourceFetcher: function () { var thisFetcher = this; var packageReadStrategy = thisFetcher._getPackageReadStrategy(); if (packageReadStrategy === 'exploded') { console.log('using new PlainExplodedFetcher'); thisFetcher.set('_resourceFetcher', new PlainExplodedFetcher({ 'baseUrl': thisFetcher.get('packageDocumentURL'), '_contentTypeDiscovery': thisFetcher.get('_contentTypeDiscovery') })); } else if (packageReadStrategy === 'zipped') { console.log('using new ZipFetcher'); thisFetcher.set('_resourceFetcher', new ZipFetcher({ 'baseUrl': thisFetcher.get('packageDocumentURL'), '_contentTypeDiscovery': thisFetcher.get('_contentTypeDiscovery'), 'libDir': thisFetcher.get('libDir') })); } else { throw new Error('Unsupported package read strategy: ' + packageReadStrategy); } thisFetcher.set('_resourceResolver', new ResourceResolver({ '_resourceFetcher': thisFetcher.get('_resourceFetcher') })); }, isPackageExploded: function () { return this.get('_resourceFetcher').isExploded(); }, resolveURI: function (epubResourceURI) { return this.get('_resourceFetcher').resolveURI(epubResourceURI); }, relativeToPackageFetchFileContents: function (relativePath, fetchMode, fetchCallback, onerror) { this.get('_resourceFetcher').relativeToPackageFetchFileContents(relativePath, fetchMode, fetchCallback, onerror); }, getPackageContentType: function () { return this.get('_packageContentType'); }, getPackageDom: function (callback) { this.get('_resourceFetcher').getPackageDom(callback); }, resolveInternalPackageResources: function (contentDocumentURI, contentDocumentType, contentDocumentText, resolvedDocumentCallback, onerror) { this.get('_resourceResolver').resolveInternalPackageResources(contentDocumentURI, contentDocumentType, contentDocumentText, resolvedDocumentCallback, onerror); } }, { contentTypePackageReadStrategyMap: { 'application/oebps-package+xml': 'exploded', 'application/epub+zip': 'zipped', 'application/zip': 'zipped' } }); return PackageFetcher; }); define('epub_fetch_module',['require', 'module', 'jquery', 'underscore', 'backbone', './models/package_fetcher' ], function (require, module, $, _, Backbone, PackageFetcher) { console.log('epub_fetch_module module id: ' + module.id); console.log(module.id + 'Backbone:' + Backbone); var EpubFetchModule = Backbone.Model.extend({ initialize: function (attributes) { this.set('packageFetcher', new PackageFetcher({ packageDocumentURL: this.get('packageDocumentURL'), libDir: this.get('libDir') })); }, // Description: The public interface getPackageContentType: function () { return this.get('packageFetcher').getPackageContentType(); }, getPackageDom: function (callback) { this.get('packageFetcher').getPackageDom(callback); }, getPackageDocumentURL: function (callback) { return this.get('packageDocumentURL'); }, isPackageExploded: function () { return this.get('packageFetcher').isPackageExploded(); }, resolveURI: function (epubResourceURI) { return this.get('packageFetcher').resolveURI(epubResourceURI); }, relativeToPackageFetchFileContents: function (relativePath, fetchMode, fetchCallback, onerror) { this.get('packageFetcher').relativeToPackageFetchFileContents(relativePath, fetchMode, fetchCallback, onerror); }, resolveInternalPackageResources: function (contentDocumentURI, contentDocumentType, contentDocumentText, resolvedDocumentCallback, onerror) { this.get('packageFetcher').resolveInternalPackageResources(contentDocumentURI, contentDocumentType, contentDocumentText, resolvedDocumentCallback, onerror); } }); return EpubFetchModule; }); define('models/manifest_item',['require', 'module', 'jquery', 'underscore', 'backbone'], function (require, module, $, _, Backbone) { var ManifestItem = Backbone.Model.extend({ isSvg: function () { return this.get("media_type") === "image/svg+xml"; }, isImage: function () { var media_type = this.get("media_type"); if (media_type && media_type.indexOf("image/") > -1) { // we want to treat svg as a special case, so they // are not images return media_type !== "image/svg+xml"; } return false; } }); return ManifestItem; }); define('models/manifest',['require', 'module', 'jquery', 'underscore', 'backbone', './manifest_item'], function (require, module, $, _, Backbone, ManifestItem) { console.log('manifest module id: ' + module.id); var Manifest = Backbone.Collection.extend({ model: ManifestItem }); return Manifest; }); define('models/spine_item',['require', 'module', 'jquery', 'underscore', 'backbone', './manifest_item'], function (require, module, $, _, Backbone, ManifestItem) { var SpineItem = ManifestItem.extend({ defaults: { "pageSpreadClass": "" }, initialize: function () { // if (this.isFixedLayout()) { // this.on("change:content", this.parseMetaTags, this); // this.loadContent(); // } }, // REFACTORING CANDIDATE: The meta tags thing has to be worked out // toJSON : function () { // var json = {}; // json.width = this.get("meta_width") || 0; // json.height = this.get("meta_height") || 0; // json.uri = this.resolveUri(this.get('href')); // json.page_class = this.getPageSpreadClass(); // return json; // }, // REFACTORING CANDIDATE: This needs to change isFixedLayout: function () { // if it an svg or image then it is fixed layout if (this.isSvg() || this.isImage()) { return true; } // if there is a fixed_flow property, then it takes precedence if (typeof this.get("fixed_flow") !== 'undefined') { return this.get("fixed_flow"); } // nothing special about this spine item, fall back to the books settings return false; }, // Description: Determines if the first page of the content document should be offset in a synthetic layout firstPageOffset: function () { // Get book properties var notFixedLayout = !this.isFixedLayout(); var pageProgDirIsRTL = this.get("page_prog_dir") === "rtl" ? true : false; var pageSpreadLeft = this.get("page_spread") === "left" ? true : false; var pageSpreadRight = this.get("page_spread") === "right" ? true : false; // Default to no page spread specified if they are both set on the spine item if (pageSpreadRight && pageSpreadLeft) { pageSpreadRight = false; pageSpreadLeft = false; } if (notFixedLayout) { if (pageProgDirIsRTL) { if (pageSpreadLeft) { return true; } } else { if (pageSpreadRight) { return true; } } } return false; }, // NOTE: Media overlays have been disabled for the time being, which is why these methods are commented out. // hasMediaOverlay : function() { // return !!this.get("media_overlay") && !!this.getMediaOverlay(); // }, // getMediaOverlay : function() { // return this.collection.getMediaOverlay(this.get("media_overlay")); // } }); return SpineItem; }); define('models/spine',['require', 'module', 'jquery', 'underscore', 'backbone', './spine_item'], function (require, module, $, _, Backbone, SpineItem) { var Spine = Backbone.Collection.extend({ model: SpineItem }); return Spine; }); define('models/metadata',['require', 'module', 'jquery', 'underscore', 'backbone'], function (require, module, $, _, Backbone) { var Metadata = Backbone.Model.extend({}); return Metadata; }); define('models/page_spread_property',['require', 'module', 'jquery', 'underscore', 'backbone'], function (require, module, $, _, Backbone) { // Description: This is a delegate that provides information about the appropriate page-spread property for fixed layout spine items var PageSpreadProperty = Backbone.Model.extend({ initialize : function () { // "Constants" for page spread class this.CENTER_PAGE = "center_page"; this.LEFT_PAGE = "left_page"; this.RIGHT_PAGE = "right_page"; }, inferiBooksPageSpread : function (spineIndex, numSpineItems) { var pageNum = spineIndex + 1; // Rationale: For ibooks, odd pages go on the right. This means // the first page will always be on the right // without a left counterpart, so center it if (pageNum === 1) { return this.CENTER_PAGE; } // Rationale: If the last spine item in the book would be on the left, then // it would have no left counterpart, so center it else if (pageNum % 2 === 0 && pageNum === numSpineItems) { return this.CENTER_PAGE; } // Rationale: Otherwise first page goes on the right, and then alternate // left - right - left - right etc else { if (pageNum % 2 === 1) { return this.RIGHT_PAGE; } else { return this.LEFT_PAGE; } } }, getPageSpreadFromProperties : function (pageSpreadProperty) { if (pageSpreadProperty === "left") { return this.LEFT_PAGE; } else if (pageSpreadProperty === "right") { return this.RIGHT_PAGE; } else if (pageSpreadProperty === "center") { return this.CENTER_PAGE; } else { return ""; } }, // NOTE: This method still cannot infer the page spread value when center pages are sporadically specified // REFACTORING CANDIDATE: Could still use some refactoring to enhance the clarity of the algorithm // Rationale: If the page spread property is not set, we must iterate back through the EPUB's spine items to find // the last spine item with a page-spread value set. We can use that value, whether there are an even or odd // number of pages between this spine item and the "last" one, and the page progression direction of the EPUB // to determine the appropriate page spread value for this spine item. inferUnassignedPageSpread : function (spineIndex, spine, pageProgDirection) { var lastSpecifiedPageSpread; var numPagesBetween; if (spine.at(spineIndex).get("page_spread") === "left" || spine.at(spineIndex).get("page_spread") === "right" || spine.at(spineIndex).get("page_spread") === "center") { return this.getPageSpreadFromProperties(spine.at(spineIndex).get("page_spread")); } // If this is the first spine item, assign left or right based on page progression direction else if (spineIndex === 0) { return pageProgDirection === "rtl" ? this.RIGHT_PAGE : this.LEFT_PAGE; } else { // Find last spine item with page-spread value and use it to determine the appropriate value for // this spine item. This loop iterates, in reverse order, from the current spine index to the // spine item that had a specified page spread specified. for (var currSpineIndex = spineIndex - 1; currSpineIndex >= 0; currSpineIndex--) { // REFACTORING CANDIDATE: This would be clearer if the currSpineIndex === 0 case was // handled seperately. if (currSpineIndex === 0 || spine.at(currSpineIndex).get("page_spread")) { lastSpecifiedPageSpread = this.lastSpecifiedPageSpread( spine.at(currSpineIndex).get("page_spread"), pageProgDirection ); numPagesBetween = spineIndex - currSpineIndex; // Even number of pages between current and last spine item if (numPagesBetween % 2 === 0) { return lastSpecifiedPageSpread === "left" ? this.LEFT_PAGE : lastSpecifiedPageSpread === "right" ? this.RIGHT_PAGE : pageProgDirection === "rtl" ? this.LEFT_PAGE : this.RIGHT_PAGE; } // Odd number of pages between current and last spine item with a specified page-spread value else { return lastSpecifiedPageSpread === "left" ? this.RIGHT_PAGE : lastSpecifiedPageSpread === "right" ? this.LEFT_PAGE : pageProgDirection === "rtl" ? this.RIGHT_PAGE : this.LEFT_PAGE; } } } } }, lastSpecifiedPageSpread : function (pageSpreadValue, pageProgDirection) { // Handles the case where currSpineIndex === 0 and a page-spread value has not been specified if (pageSpreadValue && pageSpreadValue !== "") { return pageSpreadValue; } else { return pageProgDirection === "rtl" ? "right" : "left"; } } }); return PageSpreadProperty; }); define('models/package_document_parser',['require', 'module', 'jquery', 'underscore', 'backbone'], function (require, module, $, _, Backbone) { console.log('package_document_parser module id: ' + module.id); // `PackageDocumentParser` is used to parse the xml of an epub package // document and build a javascript object. The constructor accepts an // instance of `URI` that is used to resolve paths during the process var PackageDocumentParser = Backbone.Model.extend({ initialize: function (attributes, options) { var thisParser = this; var epubFetch = thisParser.get('epubFetch'); var deferredXmlDom = $.Deferred(); thisParser.set('deferredXmlDom', deferredXmlDom); epubFetch.getPackageDom(function (packageDom) { thisParser.set('xmlDom', packageDom); deferredXmlDom.resolve(packageDom); }); }, // Parse an XML package document into a javascript object parse: function (callback) { var thisParser = this; var deferredXmlDom = thisParser.get('deferredXmlDom'); deferredXmlDom.done(function (xmlDom) { var json, manifest, cover; json = {}; json.metadata = thisParser.getJsonMetadata(xmlDom); json.bindings = thisParser.getJsonBindings(xmlDom); json.spine = thisParser.getJsonSpine(xmlDom); json.manifest = thisParser.getJsonManifest(xmlDom); // parse the page-progression-direction if it is present json.paginate_backwards = thisParser.paginateBackwards(xmlDom); // try to find a cover image cover = thisParser.getCoverHref(xmlDom); if (cover) { json.metadata.cover_href = cover; } if (json.metadata.layout === "pre-paginated") { json.metadata.fixed_layout = true; } // THIS SHOULD BE LEFT IN (BUT COMMENTED OUT), AS MO SUPPORT IS TEMPORARILY DISABLED // create a map of all the media overlay objects // json.mo_map = this.resolveMediaOverlays(json.manifest); // parse the spine into a proper collection json.spine = thisParser.parseSpineProperties(json.spine); // return the parse result callback(json); }); }, getJsonSpine: function () { var thisParser = this; var $spineElements; var jsonSpine = []; var xmlDom = thisParser.get("xmlDom"); $spineElements = $("spine", xmlDom).children(); $.each($spineElements, function (spineElementIndex, currSpineElement) { var $currSpineElement = $(currSpineElement); var spineItem = { idref: $currSpineElement.attr("idref") ? $currSpineElement.attr("idref") : "", linear: $currSpineElement.attr("linear") ? $currSpineElement.attr("linear") : "", properties: $currSpineElement.attr("properties") ? $currSpineElement.attr("properties") : "" }; jsonSpine.push(spineItem); }); return jsonSpine; }, getJsonMetadata: function () { var thisParser = this; var xmlDom = thisParser.get("xmlDom"); var $metadata = $("metadata", xmlDom); var jsonMetadata = {}; jsonMetadata.active_class = $("meta[property='media:active-class']", $metadata).text(); jsonMetadata.author = $("creator", $metadata).text(); jsonMetadata.description = $("description", $metadata).text(); jsonMetadata.epub_version = $("package", xmlDom).attr("version") ? $("package", xmlDom).attr("version") : ""; jsonMetadata.id = $("identifier", $metadata).text(); jsonMetadata.language = $("language", $metadata).text(); jsonMetadata.layout = $("meta[property='rendition:layout']", $metadata).text(); jsonMetadata.modified_date = $("meta[property='dcterms:modified']", $metadata).text(); jsonMetadata.ncx = $("spine", xmlDom).attr("toc") ? $("spine", xmlDom).attr("toc") : ""; jsonMetadata.orientation = $("meta[property='rendition:orientation']", $metadata).text(); jsonMetadata.page_prog_dir = $("spine", xmlDom).attr("page-progression-direction") ? $("spine", xmlDom).attr("page-progression-direction") : ""; jsonMetadata.pubdate = $("date", $metadata).text(); jsonMetadata.publisher = $("publisher", $metadata).text(); jsonMetadata.rights = $("rights").text(); jsonMetadata.spread = $("meta[property='rendition:spread']", $metadata).text(); jsonMetadata.title = $("title", $metadata).text(); return jsonMetadata; }, getJsonManifest: function () { var thisParser = this; var epubFetch = thisParser.get('epubFetch'); var xmlDom = thisParser.get("xmlDom"); var $manifestItems = $("manifest", xmlDom).children(); var jsonManifest = []; $.each($manifestItems, function (manifestElementIndex, currManifestElement) { var $currManifestElement = $(currManifestElement); var currManifestElementHref = $currManifestElement.attr("href") ? $currManifestElement.attr("href") : ""; var manifestItem = { contentDocumentURI: currManifestElementHref, href: currManifestElementHref, id: $currManifestElement.attr("id") ? $currManifestElement.attr("id") : "", media_overlay: $currManifestElement.attr("media-overlay") ? $currManifestElement.attr("media-overlay") : "", media_type: $currManifestElement.attr("media-type") ? $currManifestElement.attr("media-type") : "", properties: $currManifestElement.attr("properties") ? $currManifestElement.attr("properties") : "" }; // console.log('pushing manifest item to JSON manifest. currManifestElementHref: [' + currManifestElementHref + // '], manifestItem.contentDocumentURI: [' + manifestItem.contentDocumentURI + // '], manifestItem:'); // console.log(manifestItem); jsonManifest.push(manifestItem); }); return jsonManifest; }, getJsonBindings: function () { var xmlDom = this.get("xmlDom"); var $bindings = $("bindings", xmlDom).children(); var jsonBindings = []; $.each($bindings, function (bindingElementIndex, currBindingElement) { var $currBindingElement = $(currBindingElement); var binding = { handler: $currBindingElement.attr("handler") ? $currBindingElement.attr("handler") : "", media_type: $currBindingElement.attr("media-type") ? $currBindingElement.attr("media-type") : "" }; jsonBindings.push(binding); }); return jsonBindings; }, getCoverHref: function () { var dom = this.get("xmlDom"); var manifest; var $imageNode; manifest = dom.getElementsByTagName('manifest')[0]; // epub3 spec for a cover image is like this: /**/ $imageNode = $('item[properties~="cover-image"]', manifest); if ($imageNode.length === 1 && $imageNode.attr("href")) { return $imageNode.attr("href"); } // some epub2's cover image is like this: /**/ var metaNode = $('meta[name="cover"]', dom); var contentAttr = metaNode.attr("content"); if (metaNode.length === 1 && contentAttr) { $imageNode = $('item[id="' + contentAttr + '"]', manifest); if ($imageNode.length === 1 && $imageNode.attr("href")) { return $imageNode.attr("href"); } } // that didn't seem to work so, it think epub2 just uses item with id=cover $imageNode = $('#cover', manifest); if ($imageNode.length === 1 && $imageNode.attr("href")) { return $imageNode.attr("href"); } // seems like there isn't one, thats ok... return null; }, parseSpineProperties: function (spine) { var parseProperiesString = function (str) { var properties = {}; var allPropStrs = str.split(" "); // split it on white space for (var i = 0; i < allPropStrs.length; i++) { // brute force!!! //rendition:orientation landscape | portrait | auto //rendition:spread none | landscape | portrait | both | auto //rendition:page-spread-center //page-spread | left | right //rendition:layout reflowable | pre-paginated if (allPropStrs[i] === "rendition:page-spread-center") properties.page_spread = "center"; if (allPropStrs[i] === "page-spread-left") properties.page_spread = "left"; if (allPropStrs[i] === "page-spread-right") properties.page_spread = "right"; if (allPropStrs[i] === "page-spread-right") properties.page_spread = "right"; if (allPropStrs[i] === "rendition:layout-reflowable") properties.fixed_flow = false; if (allPropStrs[i] === "rendition:layout-pre-paginated") properties.fixed_flow = true; } return properties; } for (var i = 0; i < spine.length; i++) { var props = parseProperiesString(spine[i].properties); // add all the properties to the spine item _.extend(spine[i], props); } return spine; }, // resolve the url of smils on any manifest items that have a MO // attribute // NOTE: Removed media overlay support for the module refactoring // resolveMediaOverlays : function(manifest) { // var that = this; // var momap = {}; // // create a bunch of media overlay objects // manifest.forEach( function(item) { // if(item.get("media_type") === "application/smil+xml") { // var url = that.resolveUri(item.get("href")); // var moObject = new EpubParser.MediaOverlay(); // moObject.setUrl(url); // moObject.fetch(); // momap[item.id] = moObject; // } // }); // return momap; // }, // parse the EPUB3 `page-progression-direction` attribute paginateBackwards: function () { var xmlDom = this.get("xmlDom"); return $('spine', xmlDom).attr('page-progression-direction') === "rtl"; } }); return PackageDocumentParser; }); define('models/package_document',['require', 'module', 'jquery', 'underscore', 'backbone', 'URIjs/URI', './manifest', './spine', './metadata', './page_spread_property', './package_document_parser'], function (require, module, $, _, Backbone, URI, Manifest, Spine, Metadata, PageSpreadProperty, PackageDocumentParser) { console.log('package_document module id: ' + module.id); // Description: This model provides an interface for navigating an EPUB's package document var PackageDocument = Backbone.Model.extend({ initialize : function (attributes, options) { var that = this; // Initialize package document parser var packageDocParser = new PackageDocumentParser({ epubFetch : this.get("epubFetch") }); packageDocParser.parse(function (packageDocument) { that.manifest = new Manifest(packageDocument.manifest); that.spine = new Spine(packageDocument.spine); that.metadata = new Metadata(packageDocument.metadata); that.bindings = new Spine(packageDocument.bindings); that.pageSpreadProperty = new PageSpreadProperty(); // If this book is fixed layout, assign the page spread class if (that.isFixedLayout()) { that.assignPageSpreadClass(); } that.get("onParsedCallback")(); }); }, getPackageData : function () { var that = this; var spinePackageData = []; var packageDocumentURL = this.get("epubFetch").get("packageDocumentURL"); var packageDocRoot = packageDocumentURL.substr(0, packageDocumentURL.lastIndexOf("/")); this.spine.each(function (spineItem) { spinePackageData.push(that.generatePackageData(spineItem)); }); // This is where the package data format thing is generated return { rootUrl : packageDocRoot, rendition_layout : this.isFixedLayout(), spine : { direction : this.pageProgressionDirection(), items : spinePackageData } }; }, isFixedLayout : function () { if (this.metadata.get("fixed_layout")) { return true; } else { return false; } }, getManifestItemById : function (id) { var foundManifestItem = this.manifest.find( function (manifestItem) { if (manifestItem.get("id") === id) { return manifestItem; } }); if (foundManifestItem) { return foundManifestItem.toJSON(); } else { return undefined; } }, getManifestItemByIdref : function (idref) { var foundManifestItem = this.getManifestItemById(idref); if (foundManifestItem) { return foundManifestItem; } else { return undefined; } }, getSpineItemByIdref : function (idref) { var foundSpineItem = this.getSpineModelByIdref(idref); if (foundSpineItem) { return foundSpineItem.toJSON(); } else { return undefined; } }, getSpineItem : function (spineIndex) { var spineItem = this.spine.at(spineIndex); if (spineItem) { return spineItem.toJSON(); } else { return undefined; } }, spineLength : function () { return this.spine.length; }, // Description: gets the next position in the spine for which the // spineItem does not have `linear='no'`. The start // param is the non-inclusive position to begin the search // from. If start is not supplied, the search will begin at // postion 0. If no linear position can be found, this // function returns undefined getNextLinearSpinePosition : function (currSpineIndex) { var spine = this.spine; if (currSpineIndex === undefined || currSpineIndex < 0) { currSpineIndex = 0; if (spine.at(currSpineIndex).get("linear") !== "no") { return currSpineIndex; } } while (currSpineIndex < this.spineLength() - 1) { currSpineIndex += 1; if (spine.at(currSpineIndex).get("linear") !== "no") { return currSpineIndex; } } // No next linear spine position. return undefined; }, // Description: gets the previous position in the spine for which the // spineItem does not have `linear='no'`. The start // param is the non-inclusive position to begin the search // from. If start is not supplied, the search will begin at // the end of the spine. If no linear position can be found, // this function returns undefined getPrevLinearSpinePosition : function(currSpineIndex) { var spine = this.spine; if (currSpineIndex === undefined || currSpineIndex > this.spineLength() - 1) { currSpineIndex = this.spineLength() - 1; if (spine.at(currSpineIndex).get("linear") !== "no") { return currSpineIndex; } } while (currSpineIndex > 0) { currSpineIndex -= 1; if (spine.at(currSpineIndex).get("linear") !== "no") { return currSpineIndex; } } // No previous linear spine position. return undefined; }, hasNextSection: function(currSpineIndex) { if (currSpineIndex >= 0 && currSpineIndex <= this.spineLength() - 1) { return this.getNextLinearSpinePosition(currSpineIndex) > -1; } else { return false; } }, hasPrevSection: function(currSpineIndex) { if (currSpineIndex >= 0 && currSpineIndex <= this.spineLength() - 1) { return this.getPrevLinearSpinePosition(currSpineIndex) > -1; } else { return false; } }, pageProgressionDirection : function () { if (this.metadata.get("page_prog_dir") === "rtl") { return "rtl"; } else if (this.metadata.get("page_prog_dir") === "default") { return "default"; } else { return "ltr"; } }, getSpineIndexByHref : function (manifestHref) { var spineItem = this.getSpineModelFromHref(manifestHref); return this.getSpineIndex(spineItem); }, getBindingByHandler : function (handler) { var binding = this.bindings.find( function (binding) { if (binding.get("handler") === handler) { return binding; } }); if (binding) { return binding.toJSON(); } else { return undefined; } }, generatePackageData : function (spineItem) { var fixedLayoutProperty = "reflowable"; // var fixedLayoutType = undefined; var manifestItem = this.getManifestModelByIdref(spineItem.get("idref")); // var isLinear; // var firstPageIsOffset; var pageSpread; // Get fixed layout properties if (spineItem.isFixedLayout() || this.isFixedLayout()) { isFixedLayout = true; fixedLayoutProperty = "pre-paginated"; // if (manifestItem.isSvg()) { // fixedLayoutType = "svg"; // } // else if (manifestItem.isImage()) { // fixedLayoutType = "image"; // } // else { // fixedLayoutType = "xhtml"; // } } // Set primary reading order attribute // if (spineItem.get("linear").trim() === "no") { // isLinear = false; // } // else { // isLinear = true; // } pageSpread = spineItem.get("page_spread"); // Set first page is offset parameter // if (!isFixedLayout) { // if (this.pageProgressionDirection() === "ltr" && pageSpread === "right") { // firstPageIsOffset = true; // } // else if (this.pageProgressionDirection() === "rtl" && pageSpread === "left") { // firstPageIsOffset = true; // } // else { // firstPageIsOffset = false; // } // } if (pageSpread === "left") { pageSpread = "page-spread-left"; } else if (pageSpread === "right") { pageSpread = "page-spread-right"; } else if (pageSpread === "center") { pageSpread = "page-spread-center"; } var spineInfo = { href : manifestItem.get('contentDocumentURI'), media_type : manifestItem.get('media_type'), media_overlay : manifestItem.get('media_overlay'), idref : spineItem.get("idref"), page_spread : pageSpread, rendition_layout : fixedLayoutProperty }; return spineInfo; }, // TODO apparently unused method, and in the incorrect module (should be in epub-parser?) getPackageDocumentDOM : function (callback) { this.get('epubFetch').getPackageDom(callback); }, getToc : function () { var item = this.getTocItem(); if (item) { var href = item.get("contentDocumentURI"); return href; } return null; }, getTocText: function (callback) { var tocUrl = this.getToc(); console.log('tocUrl: [' + tocUrl + ']'); this.get('epubFetch').relativeToPackageFetchFileContents(tocUrl, 'text', function (tocDocumentText) { callback(tocDocumentText) }, function (err) { console.error('ERROR fetching TOC from [' + this.getToc() + ']:'); console.error(err); callback(undefined); }); }, getTocDom: function (callback) { this.getTocText(function (tocText) { if (typeof tocText === 'string') { var tocDom = (new DOMParser()).parseFromString(tocText, "text/xml"); callback(tocDom); } else { callback(undefined); } }); }, // Description: This is a convenience method that will generate an html list structure from an ncx XML // document. generateTocListDOM: function (callback) { var that = this; that.getTocDom(function (tocDom) { if (tocDom) { if (that.tocIsNcx()) { var $ncxOrderedList; $ncxOrderedList = that.getNcxOrderedList($("navMap", tocDom)); callback($ncxOrderedList[0]); } else { var packageDocumentURL = that.get('epubFetch').getPackageDocumentURL(); var packageDocumentAbsoluteURL = new URI(packageDocumentURL).absoluteTo(document.URL); var tocDocumentAbsoluteURL = new URI(that.getToc()).absoluteTo(document.URL); // add a BASE tag to change the TOC document's baseURI. var oldBaseTag = $(tocDom).remove('base'); var newBaseTag = $(''); $(newBaseTag).attr('href', tocDocumentAbsoluteURL); $(tocDom).find('head').append(newBaseTag); // TODO: fix TOC hrefs both for exploded in zipped EPUBs callback(tocDom); } } else { callback(undefined); } }); }, tocIsNcx : function () { var tocItem = this.getTocItem(); var contentDocURI = tocItem.get("contentDocumentURI"); var fileExtension = contentDocURI.substr(contentDocURI.lastIndexOf('.') + 1); if (fileExtension.trim().toLowerCase() === "ncx") { return true; } else { return false; } }, // ----------------------- PRIVATE HELPERS -------------------------------- // getNcxOrderedList : function ($navMapDOM) { var that = this; var $ol = $("
    "); $.each($navMapDOM.children("navPoint"), function (index, navPoint) { that.addNavPointElements($(navPoint), $ol); }); return $ol; }, // Description: Constructs an html representation of NCX navPoints, based on an object of navPoint information // Rationale: This is a recursive method, as NCX navPoint elements can nest 0 or more of themselves as children addNavPointElements : function ($navPointDOM, $ol) { var that = this; // Add the current navPoint element to the TOC html var navText = $navPointDOM.children("navLabel").text().trim(); var navHref = $navPointDOM.children("content").attr("src"); var $navPointLi = $(""); // Append nav point info $ol.append($navPointLi); // Append ordered list of nav points if ($navPointDOM.children("navPoint").length > 0 ) { var $newLi = $("
  1. "); var $newOl = $("
      "); $.each($navPointDOM.children("navPoint"), function (navIndex, navPoint) { $newOl.append(that.addNavPointElements($(navPoint), $newOl)); }); $newLi.append($newOl); $ol.append($newLi); } }, // Refactoring candidate: This search will always iterate through entire manifest; this should be modified to // return when the manifest item is found. getSpineModelFromHref : function (manifestHref) { var that = this; var resourceURI = new URI(manifestHref); var resourceName = resourceURI.filename(); var foundSpineModel; this.manifest.each(function (manifestItem) { var manifestItemURI = new URI(manifestItem.get("href")); var manifestItemName = manifestItemURI.filename(); // Rationale: Return a spine model based on the manifest item id, which is the idref of the spine item if (manifestItemName === resourceName) { foundSpineModel = that.getSpineModelByIdref(manifestItem.get("id")); } }); return foundSpineModel; }, getSpineModelByIdref : function (idref) { var foundSpineItem = this.spine.find( function (spineItem) { if (spineItem.get("idref") === idref) { return spineItem; } }); return foundSpineItem; }, getManifestModelByIdref : function (idref) { var foundManifestItem = this.manifest.find( function (manifestItem) { if (manifestItem.get("id") === idref) { return manifestItem; } }); return foundManifestItem; }, getSpineIndex : function (spineItem) { return this.spine.indexOf(spineItem); }, // Description: When rendering fixed layout pages we need to determine whether the page // should be on the left or the right in two up mode, options are: // left_page: render on the left side // right_page: render on the right side // center_page: always center the page horizontally // This property must be assigned when the package document is initialized // NOTE: Look into how spine items with the linear="no" property affect this algorithm assignPageSpreadClass : function () { var that = this; var pageSpreadClass; var numSpineItems; // If the epub is apple fixed layout if (this.metadata.get("apple_fixed")) { numSpineItems = this.spine.length; this.spine.each(function (spineItem, spineIndex) { pageSpreadClass = that.pageSpreadProperty.inferiBooksPageSpread(spineIndex, numSpineItems); spineItem.set({ pageSpreadClass : pageSpreadClass }); }); } else { // For each spine item this.spine.each(function (spineItem, spineIndex) { if (spineItem.get("page_spread")) { pageSpreadClass = that.pageSpreadProperty.getPageSpreadFromProperties(spineItem.get("page_spread")); spineItem.set({ pageSpreadClass : pageSpreadClass }); } else { pageSpreadClass = that.pageSpreadProperty.inferUnassignedPageSpread(spineIndex, that.spine, that.pageProgressionDirection()); spineItem.set({ pageSpreadClass : pageSpreadClass }); } }); } }, getTocItem : function() { var manifest = this.manifest; var metadata = this.metadata; var spine_id = this.metadata.get("ncx"); var item = manifest.find(function(item){ if (item.get("properties").indexOf("nav") !== -1) { return true; } else { return false; } }); if( item ) { return item; } if( spine_id && spine_id.length > 0 ) { return manifest.find(function(item) { return item.get("id") === spine_id; }); } return null; } // NOTE: Media overlays are temporarily disabled // getMediaOverlayItem : function(idref) { // // just look up the object in the mo_map // var map = this.get("mo_map"); // return map && map[idref]; // }, }); return PackageDocument; }); define('epub_module',['require', 'module', 'jquery', 'underscore', 'backbone', './models/package_document' ], function (require, module, $, _, Backbone, PackageDocument) { var EpubModule = function (epubFetch, callback) { var packageDoc = new PackageDocument({ epubFetch : epubFetch, onParsedCallback : callback }); // Description: The public interface return { getPackageData: function () { return packageDoc.getPackageData(); }, isFixedLayout: function () { return packageDoc.isFixedLayout(); }, getManifestItemById: function (id) { return packageDoc.getManifestItemById(id); }, getManifestItemByIdref: function (idref) { return packageDoc.getManifestItemByIdref(idref); }, getSpineItemByIdref: function (idref) { return packageDoc.getSpineItemByIdref(idref); }, getSpineItemByIndex: function (spineIndex) { return packageDoc.getSpineItem(spineIndex); }, spineLength: function () { return packageDoc.spineLength(); }, getNextLinearSpinePosition: function (currSpineIndex) { return packageDoc.getNextLinearSpinePosition(currSpineIndex); }, getPrevLinearSpinePosition: function (currSpineIndex) { return packageDoc.getPrevLinearSpinePosition(currSpineIndex); }, hasNextSection: function (currSpineIndex) { return packageDoc.hasNextSection(currSpineIndex); }, hasPrevSection: function (currSpineIndex) { return packageDoc.hasPrevSection(currSpineIndex); }, pageProgressionDirection: function () { return packageDoc.pageProgressionDirection(); }, getSpineIndexByHref: function (manifestHref) { return packageDoc.getSpineIndexByHref(manifestHref); }, getTocURL: function () { return packageDoc.getToc(); }, getTocText: function (callback) { return packageDoc.getTocText(callback); }, getTocDom: function (callback) { return packageDoc.getTocDom(callback); }, generateTocListDOM: function (callback) { return packageDoc.generateTocListDOM(callback); }, tocIsNcx: function () { return packageDoc.tocIsNcx(); } }; }; return EpubModule; }); define('epub_reading_system',['require', 'module'], function (require, module) { // Taken from https://raw.github.com/readium/readium/master/scripts/epub_reading_system.js // The epubReadingSystem object provides an interface through which a Scripted Content // Document can query information about a User's Reading System. // // More information is available [here](http://idpf.org/epub/30/spec/epub30-contentdocs.html#app-epubReadingSystem) // Has to be global per spec. navigator.epubReadingSystem = { name: "Readium.js", version: "0.0.1", layoutStyle: "paginated", hasFeature: function (feature, version) { // for now all features must be version 1.0 so fail fast if the user has asked for something else if (version && version !== "1.0") { return false; } if (feature === "dom-manipulation") { // Scripts may make structural changes to the document’s DOM (applies to spine-level scripting only). return true; } if (feature === "layout-changes") { // Scripts may modify attributes and CSS styles that affect content layout (applies to spine-level scripting only). return true; } if (feature === "touch-events") { // The device supports touch events and the Reading System passes touch events to the content. return false; } if (feature === "mouse-events") { // The device supports mouse events and the Reading System passes mouse events to the content. return true; } if (feature === "keyboard-events") { // The device supports keyboard events and the Reading System passes keyboard events to the content. return true; } if (feature === "spine-scripting") { //Spine-level scripting is supported. return true; } return false; } } return navigator.epubReadingSystem; }); define('epub_renderer_module', ['require', 'module', 'jquery', 'underscore', 'backbone', 'URIjs/URI'], function (require, module, $, _, Backbone, URI) { // LauncherOSX // // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /** * Top level ReadiumSDK namespace * @class ReadiumSDK * @static */ ReadiumSDK = { /** Current version of the JS SDK @method version @static @return {string} version */ version: function() { return "0.5.1"; }, Models : {}, Views : {}, Collections: {}, Routers: {}, Helpers: {} }; _.extend(ReadiumSDK, Backbone.Events); define("readiumSDK", function(){}); // LauncherOSX // // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ReadiumSDK.Helpers.Rect = function(left, top, width, height) { this.left = left; this.top = top; this.width = width; this.height = height; this.right = function () { return this.left + this.width; }; this.bottom = function() { return this.top + this.height; }; this.isOverlap = function(rect, tolerance) { if(tolerance == undefined) { tolerance = 0; } return !(rect.right() < this.left + tolerance || rect.left > this.right() - tolerance || rect.bottom() < this.top + tolerance || rect.top > this.bottom() - tolerance); } }; //This method treats multicolumn view as one long column and finds the rectangle of the element in this "long" column //we are not using jQuery Offset() and width()/height() function because for multicolumn rendition_layout it produces rectangle as a bounding box of element that // reflows between columns this is inconstant and difficult to analyze . ReadiumSDK.Helpers.Rect.fromElement = function($element) { var e = $element[0]; var offsetLeft = e.offsetLeft; var offsetTop = e.offsetTop; var offsetWidth = e.offsetWidth; var offsetHeight = e.offsetHeight; while(e = e.offsetParent) { offsetLeft += e.offsetLeft; offsetTop += e.offsetTop; } return new ReadiumSDK.Helpers.Rect(offsetLeft, offsetTop, offsetWidth, offsetHeight); }; ReadiumSDK.Helpers.LoadIframe = function(iframe, src, callback, context) { var isWaitingForFrameLoad = true; iframe.onload = function() { isWaitingForFrameLoad = false; callback.call(context, true); }; //yucks! iframe doesn't trigger onerror event - there is no reliable way to know that iframe finished // attempt tot load resource (successfully or not; window.setTimeout(function(){ if(isWaitingForFrameLoad) { isWaitingForFrameLoad = false; callback.call(context, false); } }, 500); iframe.src = src; }; /** * @return {string} */ ReadiumSDK.Helpers.ResolveContentRef = function(contentRef, sourceFileHref) { if(!sourceFileHref) { return contentRef; } var sourceParts = sourceFileHref.split("/"); sourceParts.pop(); //remove source file name var pathComponents = contentRef.split("/"); while(sourceParts.length > 0 && pathComponents[0] === "..") { sourceParts.pop(); pathComponents.splice(0, 1); } var combined = sourceParts.concat(pathComponents); return combined.join("/"); }; /** * @return {boolean} */ ReadiumSDK.Helpers.EndsWith = function (str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; }; define("helpers", function(){}); // LauncherOSX // // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Setter fot epub Triggers * * * @param domNode */ ReadiumSDK.Models.Trigger = function(domNode) { var $el = $(domNode); this.action = $el.attr("action"); this.ref = $el.attr("ref"); this.event = $el.attr("ev:event"); this.observer = $el.attr("ev:observer"); this.ref = $el.attr("ref"); }; ReadiumSDK.Models.Trigger.prototype.subscribe = function(dom) { var selector = "#" + this.observer; var that = this; $(selector, dom).on(this.event, function() { that.execute(dom); }); }; ReadiumSDK.Models.Trigger.prototype.execute = function(dom) { var $target = $( "#" + this.ref, dom); switch(this.action) { case "show": $target.css("visibility", "visible"); break; case "hide": $target.css("visibility", "hidden"); break; case "play": $target[0].currentTime = 0; $target[0].play(); break; case "pause": $target[0].pause(); break; case "resume": $target[0].play(); break; case "mute": $target[0].muted = true; break; case "unmute": $target[0].muted = false; break; default: console.log("do not no how to handle trigger " + this.action); } }; define("triggers", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /** @class ReadiumSDK.Models.BookmarkData */ ReadiumSDK.Models.BookmarkData = function(idref, contentCFI) { /** * spine item idref * @property idref * @type {string} */ this.idref = idref; /** * cfi of the first visible element * @property contentCFI * @type {string} */ this.contentCFI = contentCFI; }; define("bookmarkData", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Wrapper of the SpineItem object received from the host application * * @class ReadiumSDK.Models.SpineItem * * @param itemData spine item properties container * @param {Number} index * @param {ReadiumSDK.Models.Spine} spine * */ ReadiumSDK.Models.SpineItem = function(itemData, index, spine){ this.idref = itemData.idref; this.href = itemData.href; this.page_spread = itemData.page_spread; this.rendition_layout = itemData.rendition_layout; this.index = index; this.spine = spine; this.isLeftPage = function() { return this.page_spread === "page-spread-left"; }; this.isRightPage = function() { return this.page_spread === "page-spread-right"; }; this.isCenterPage = function() { return !this.isLeftPage() && !this.isRightPage(); }; this.isReflowable = function() { return !this.isFixedLayout(); }; this.isFixedLayout = function() { return this.rendition_layout ? this.rendition_layout === "pre-paginated" : this.spine.package.isFixedLayout(); } }; define("spineItem", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Wrapper of the spine object received from hosting application * * @class ReadiumSDK.Models.Spine */ ReadiumSDK.Models.Spine = Backbone.Model.extend({ /* * Collection of spine items * @property items * @type {Array} */ items: [], /* * Page progression direction ltr|rtl|default * @property direction * @type {string} */ direction: undefined, /* * @property package * @type {ReadiumSDK.Models.Package} * */ package: undefined, initialize : function() { this.reset(); this.package = this.get("package"); var spineData = this.get("spineData"); if(spineData) { this.direction = spineData.direction; if(!this.direction) { this.direction = "ltr"; } var length = spineData.items.length; for(var i = 0; i < length; i++) { var item = new ReadiumSDK.Models.SpineItem(spineData.items[i], i, this); this.items.push(item); } } }, reset: function() { this.items = []; this.direction = undefined; this.package = undefined; }, prevItem: function(item) { if(this.isValidIndex(item.index - 1)) { return this.items[item.index - 1]; } return undefined; }, nextItem: function(item){ if(this.isValidIndex(item.index + 1)) { return this.items[item.index + 1]; } return undefined; }, getItemUrl: function(item) { if(this.package.rootUrl) { if(ReadiumSDK.Helpers.EndsWith(this.package.rootUrl, "/")){ return this.package.rootUrl + item.href; } else { return this.package.rootUrl + "/" + item.href; } } return item.href; }, isValidIndex: function(index) { return index >= 0 && index < this.items.length; }, first: function() { return this.items[0]; }, last: function() { return this.items[this.items.length - 1]; }, item: function(index) { return this.item(index); }, isRightToLeft: function() { return this.direction == "rtl"; }, isLeftToRight: function() { return !this.isRightToLeft(); }, getItemById: function(idref) { var length = this.items.length; for(var i = 0; i < length; i++) { if(this.items[i].idref == idref) { return this.items[i]; } } return undefined; }, getItemByHref: function(href) { var length = this.items.length; for(var i = 0; i < length; i++) { if(this.items[i].href == href) { return this.items[i]; } } return undefined; } }); define("spine", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * * * * @param {ReadiumSDK.Models.Spine} spine * @constructor */ ReadiumSDK.Models.Spread = function(spine) { this.spine = spine; this.leftItem = undefined; this.rightItem = undefined; this.centerItem = undefined; this.isSyntheticSpread = true; this.setSyntheticSpread = function(isSyntheticSpread) { this.isSyntheticSpread = isSyntheticSpread; }; this.openFirst = function() { if( this.spine.items.length == 0 ) { this.resetItems(); } else { this.openItem(this.spine.first()); } }; this.openLast = function() { if( this.spine.items.length == 0 ) { this.resetItems(); } else { this.openItem(this.spine.last()); } }; this.openItem = function(item) { this.resetItems(); this.setItem(item); var neighbourItem = this.getNeighbourItem(item); if(neighbourItem) { this.setItem(neighbourItem); } }; this.resetItems = function() { this.leftItem = undefined; this.rightItem = undefined; this.centerItem = undefined; }; this.setItem = function(item) { if(!this.isSyntheticSpread) { this.centerItem = item; return; } if(item.isLeftPage()) { this.leftItem = item; } else if (item.isRightPage()) { this.rightItem = item; } else { this.centerItem = item; } }; this.openNext = function() { var items = this.validItems(); if(items.length == 0) { this.openFirst(); } else { var nextItem = this.spine.nextItem(items[items.length - 1]); if(nextItem) { this.openItem(nextItem); } } } this.openPrev = function() { var items = this.validItems(); if(items.length == 0) { this.openLast(); } else { var prevItem = this.spine.prevItem(items[0]); if(prevItem) { this.openItem(prevItem); } } }; this.validItems = function() { var arr = []; if(this.leftItem) arr.push(this.leftItem); if(this.rightItem) arr.push(this.rightItem); if(this.centerItem) arr.push(this.centerItem); arr.sort(function(a, b) { return a.index - b.index; }); return arr; } this.getNeighbourItem = function(item) { var neighbourItem = undefined; if(!this.isSyntheticSpread) { return neighbourItem; } if(item.isLeftPage()) { neighbourItem = this.spine.isRightToLeft() ? this.spine.prevItem(item) : this.spine.nextItem(item); } else if(item.isRightPage()) { neighbourItem = this.spine.isRightToLeft() ? this.spine.nextItem(item) : this.spine.prevItem(item); } if(neighbourItem && (neighbourItem.isCenterPage() || neighbourItem.page_spread === item.page_spread) ) { neighbourItem = undefined; } return neighbourItem; }; }; define("fixedPageSpread", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * * @class ReadiumSDK.Models.Package */ ReadiumSDK.Models.Package = Backbone.Model.extend({ spine: undefined, rendition_layout: undefined, rootUrl: undefined, initialize : function() { this.reset(); var packageData = this.get("packageData"); if(packageData) { this.rootUrl = packageData.rootUrl; this.rendition_layout = packageData.rendition_layout; if(!this.rendition_layout) { this.rendition_layout = "reflowable"; } this.spine = new ReadiumSDK.Models.Spine({spineData: packageData.spine, package: this}); } }, reset: function() { this.spine = undefined; this.rendition_layout = undefined; this.rootUrl = undefined; }, isFixedLayout: function() { return this.rendition_layout === "pre-paginated"; }, isReflowable: function() { return !this.isFixedLayout(); } }); define("package", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ReadiumSDK.Models.ViewerSettings = function(settingsData) { this.isSyntheticSpread = true; this.fontSize = 100; this.columnGap = 20; this.update = function(settingsData) { if(settingsData.isSyntheticSpread !== undefined) { this.isSyntheticSpread = settingsData.isSyntheticSpread; } if(settingsData.columnGap !== undefined) { this.columnGap = settingsData.columnGap; } if(settingsData.fontSize !== undefined) { this.fontSize = settingsData.fontSize; } }; this.update(settingsData); }; define("viewerSettings", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* Used to report pagination state back to the host application @class ReadiumSDK.Models.CurrentPagesInfo @constructor @param {Number} spineItemCount Number of spine items @param {boolean} isFixedLayout is fixed or reflowable spine item @param {string} pageProgressionDirection ltr | rtl */ ReadiumSDK.Models.CurrentPagesInfo = function(spineItemCount, isFixedLayout, pageProgressionDirection) { this.pageProgressionDirection = pageProgressionDirection; this.isFixedLayout = isFixedLayout; this.spineItemCount = spineItemCount; this.openPages = []; this.addOpenPage = function(spineItemPageIndex, spineItemPageCount, idref, spineItemIndex) { this.openPages.push({spineItemPageIndex: spineItemPageIndex, spineItemPageCount: spineItemPageCount, idref: idref, spineItemIndex: spineItemIndex}); this.sort(); }; this.sort = function() { this.openPages.sort(function(a, b) { if(a.spineItemIndex != b.spineItemIndex) { return a.spineItemIndex - b.spineItemIndex; } return a.pageIndex - b.pageIndex; }); }; }; define("currentPagesInfo", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Representation of opening page request * Provides the spine item to be opened and one of the following properties: * spineItemPageIndex {Number}, * elementId {String}, * elementCfi {String}, * firstPage {bool}, * lastPage {bool} * * @param {ReadiumSDK.Models.SpineItem} spineItem * * @constructor */ ReadiumSDK.Models.PageOpenRequest = function(spineItem) { this.spineItem = spineItem; this.spineItemPageIndex = undefined; this.elementId = undefined; this.elementCfi = undefined; this.firstPage = false; this.lastPage = false; this.reset = function() { this.spineItemPageIndex = undefined; this.elementId = undefined; this.elementCfi = undefined; this.firstPage = false; this.lastPage = false; }; this.setFirstPage = function() { this.reset(); this.firstPage = true; }; this.setLastPage = function() { this.reset(); this.lastPage = true; }; this.setPageIndex = function(pageIndex) { this.reset(); this.spineItemPageIndex = pageIndex; }; this.setElementId = function(elementId) { this.reset(); this.elementId = elementId; }; this.setElementCfi = function(elementCfi) { this.reset(); this.elementCfi = elementCfi; }; }; define("pageOpenRequest", function(){}); (function(global) { var EPUBcfi = {}; EPUBcfi.Parser = (function(){ /* * Generated by PEG.js 0.7.0. * * http://pegjs.majda.cz/ */ function quote(s) { /* * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a * string literal except for the closing quote character, backslash, * carriage return, line separator, paragraph separator, and line feed. * Any character may appear in the form of an escape sequence. * * For portability, we also escape escape all control and non-ASCII * characters. Note that "\0" and "\v" escape sequences are not used * because JSHint does not like the first and IE the second. */ return '"' + s .replace(/\\/g, '\\\\') // backslash .replace(/"/g, '\\"') // closing quote character .replace(/\x08/g, '\\b') // backspace .replace(/\t/g, '\\t') // horizontal tab .replace(/\n/g, '\\n') // line feed .replace(/\f/g, '\\f') // form feed .replace(/\r/g, '\\r') // carriage return .replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape) + '"'; } var result = { /* * Parses the input with a generated parser. If the parsing is successfull, * returns a value explicitly or implicitly specified by the grammar from * which the parser was generated (see |PEG.buildParser|). If the parsing is * unsuccessful, throws |PEG.parser.SyntaxError| describing the error. */ parse: function(input, startRule) { var parseFunctions = { "fragment": parse_fragment, "path": parse_path, "local_path": parse_local_path, "indexStep": parse_indexStep, "indirectionStep": parse_indirectionStep, "terminus": parse_terminus, "idAssertion": parse_idAssertion, "textLocationAssertion": parse_textLocationAssertion, "parameter": parse_parameter, "csv": parse_csv, "valueNoSpace": parse_valueNoSpace, "value": parse_value, "escapedSpecialChars": parse_escapedSpecialChars, "number": parse_number, "integer": parse_integer, "space": parse_space, "circumflex": parse_circumflex, "doubleQuote": parse_doubleQuote, "squareBracket": parse_squareBracket, "parentheses": parse_parentheses, "comma": parse_comma, "semicolon": parse_semicolon, "equal": parse_equal, "character": parse_character }; if (startRule !== undefined) { if (parseFunctions[startRule] === undefined) { throw new Error("Invalid rule name: " + quote(startRule) + "."); } } else { startRule = "fragment"; } var pos = 0; var reportFailures = 0; var rightmostFailuresPos = 0; var rightmostFailuresExpected = []; function padLeft(input, padding, length) { var result = input; var padLength = length - input.length; for (var i = 0; i < padLength; i++) { result = padding + result; } return result; } function escape(ch) { var charCode = ch.charCodeAt(0); var escapeChar; var length; if (charCode <= 0xFF) { escapeChar = 'x'; length = 2; } else { escapeChar = 'u'; length = 4; } return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length); } function matchFailed(failure) { if (pos < rightmostFailuresPos) { return; } if (pos > rightmostFailuresPos) { rightmostFailuresPos = pos; rightmostFailuresExpected = []; } rightmostFailuresExpected.push(failure); } function parse_fragment() { var result0, result1, result2; var pos0, pos1; pos0 = pos; pos1 = pos; if (input.substr(pos, 8) === "epubcfi(") { result0 = "epubcfi("; pos += 8; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"epubcfi(\""); } } if (result0 !== null) { result1 = parse_path(); if (result1 !== null) { if (input.charCodeAt(pos) === 41) { result2 = ")"; pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("\")\""); } } if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, pathVal) { return { type:"CFIAST", cfiString:pathVal }; })(pos0, result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_path() { var result0, result1; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_indexStep(); if (result0 !== null) { result1 = parse_local_path(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, stepVal, localPathVal) { return { type:"cfiString", path:stepVal, localPath:localPathVal }; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_local_path() { var result0, result1; var pos0, pos1; pos0 = pos; pos1 = pos; result1 = parse_indexStep(); if (result1 === null) { result1 = parse_indirectionStep(); } if (result1 !== null) { result0 = []; while (result1 !== null) { result0.push(result1); result1 = parse_indexStep(); if (result1 === null) { result1 = parse_indirectionStep(); } } } else { result0 = null; } if (result0 !== null) { result1 = parse_terminus(); result1 = result1 !== null ? result1 : ""; if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, localPathStepVal, termStepVal) { return { steps:localPathStepVal, termStep:termStepVal }; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_indexStep() { var result0, result1, result2, result3, result4; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; if (input.charCodeAt(pos) === 47) { result0 = "/"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"/\""); } } if (result0 !== null) { result1 = parse_integer(); if (result1 !== null) { pos2 = pos; if (input.charCodeAt(pos) === 91) { result2 = "["; pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("\"[\""); } } if (result2 !== null) { result3 = parse_idAssertion(); if (result3 !== null) { if (input.charCodeAt(pos) === 93) { result4 = "]"; pos++; } else { result4 = null; if (reportFailures === 0) { matchFailed("\"]\""); } } if (result4 !== null) { result2 = [result2, result3, result4]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } result2 = result2 !== null ? result2 : ""; if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, stepLengthVal, assertVal) { return { type:"indexStep", stepLength:stepLengthVal, idAssertion:assertVal[1] }; })(pos0, result0[1], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_indirectionStep() { var result0, result1, result2, result3, result4; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; if (input.substr(pos, 2) === "!/") { result0 = "!/"; pos += 2; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"!/\""); } } if (result0 !== null) { result1 = parse_integer(); if (result1 !== null) { pos2 = pos; if (input.charCodeAt(pos) === 91) { result2 = "["; pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("\"[\""); } } if (result2 !== null) { result3 = parse_idAssertion(); if (result3 !== null) { if (input.charCodeAt(pos) === 93) { result4 = "]"; pos++; } else { result4 = null; if (reportFailures === 0) { matchFailed("\"]\""); } } if (result4 !== null) { result2 = [result2, result3, result4]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } result2 = result2 !== null ? result2 : ""; if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, stepLengthVal, assertVal) { return { type:"indirectionStep", stepLength:stepLengthVal, idAssertion:assertVal[1] }; })(pos0, result0[1], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_terminus() { var result0, result1, result2, result3, result4; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; if (input.charCodeAt(pos) === 58) { result0 = ":"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\":\""); } } if (result0 !== null) { result1 = parse_integer(); if (result1 !== null) { pos2 = pos; if (input.charCodeAt(pos) === 91) { result2 = "["; pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("\"[\""); } } if (result2 !== null) { result3 = parse_textLocationAssertion(); if (result3 !== null) { if (input.charCodeAt(pos) === 93) { result4 = "]"; pos++; } else { result4 = null; if (reportFailures === 0) { matchFailed("\"]\""); } } if (result4 !== null) { result2 = [result2, result3, result4]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } result2 = result2 !== null ? result2 : ""; if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, textOffsetValue, textLocAssertVal) { return { type:"textTerminus", offsetValue:textOffsetValue, textAssertion:textLocAssertVal[1] }; })(pos0, result0[1], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_idAssertion() { var result0; var pos0; pos0 = pos; result0 = parse_value(); if (result0 !== null) { result0 = (function(offset, idVal) { return idVal; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_textLocationAssertion() { var result0, result1; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_csv(); result0 = result0 !== null ? result0 : ""; if (result0 !== null) { result1 = parse_parameter(); result1 = result1 !== null ? result1 : ""; if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, csvVal, paramVal) { return { type:"textLocationAssertion", csv:csvVal, parameter:paramVal }; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_parameter() { var result0, result1, result2, result3; var pos0, pos1; pos0 = pos; pos1 = pos; if (input.charCodeAt(pos) === 59) { result0 = ";"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\";\""); } } if (result0 !== null) { result1 = parse_valueNoSpace(); if (result1 !== null) { if (input.charCodeAt(pos) === 61) { result2 = "="; pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("\"=\""); } } if (result2 !== null) { result3 = parse_valueNoSpace(); if (result3 !== null) { result0 = [result0, result1, result2, result3]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, paramLHSVal, paramRHSVal) { return { type:"parameter", LHSValue:paramLHSVal, RHSValue:paramRHSVal }; })(pos0, result0[1], result0[3]); } if (result0 === null) { pos = pos0; } return result0; } function parse_csv() { var result0, result1, result2; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_value(); result0 = result0 !== null ? result0 : ""; if (result0 !== null) { if (input.charCodeAt(pos) === 44) { result1 = ","; pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("\",\""); } } if (result1 !== null) { result2 = parse_value(); result2 = result2 !== null ? result2 : ""; if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, preAssertionVal, postAssertionVal) { return { type:"csv", preAssertion:preAssertionVal, postAssertion:postAssertionVal }; })(pos0, result0[0], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_valueNoSpace() { var result0, result1; var pos0; pos0 = pos; result1 = parse_escapedSpecialChars(); if (result1 === null) { result1 = parse_character(); } if (result1 !== null) { result0 = []; while (result1 !== null) { result0.push(result1); result1 = parse_escapedSpecialChars(); if (result1 === null) { result1 = parse_character(); } } } else { result0 = null; } if (result0 !== null) { result0 = (function(offset, stringVal) { return stringVal.join(''); })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_value() { var result0, result1; var pos0; pos0 = pos; result1 = parse_escapedSpecialChars(); if (result1 === null) { result1 = parse_character(); if (result1 === null) { result1 = parse_space(); } } if (result1 !== null) { result0 = []; while (result1 !== null) { result0.push(result1); result1 = parse_escapedSpecialChars(); if (result1 === null) { result1 = parse_character(); if (result1 === null) { result1 = parse_space(); } } } } else { result0 = null; } if (result0 !== null) { result0 = (function(offset, stringVal) { return stringVal.join(''); })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_escapedSpecialChars() { var result0, result1; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_circumflex(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 === null) { pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_squareBracket(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 === null) { pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_parentheses(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 === null) { pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_comma(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 === null) { pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_semicolon(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 === null) { pos1 = pos; result0 = parse_circumflex(); if (result0 !== null) { result1 = parse_equal(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } } } } } if (result0 !== null) { result0 = (function(offset, escSpecCharVal) { return escSpecCharVal[1]; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_number() { var result0, result1, result2, result3; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; pos2 = pos; if (/^[1-9]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[1-9]"); } } if (result0 !== null) { if (/^[0-9]/.test(input.charAt(pos))) { result2 = input.charAt(pos); pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } if (result2 !== null) { result1 = []; while (result2 !== null) { result1.push(result2); if (/^[0-9]/.test(input.charAt(pos))) { result2 = input.charAt(pos); pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } } } else { result1 = null; } if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos2; } } else { result0 = null; pos = pos2; } if (result0 !== null) { if (input.charCodeAt(pos) === 46) { result1 = "."; pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("\".\""); } } if (result1 !== null) { pos2 = pos; result2 = []; if (/^[0-9]/.test(input.charAt(pos))) { result3 = input.charAt(pos); pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } while (result3 !== null) { result2.push(result3); if (/^[0-9]/.test(input.charAt(pos))) { result3 = input.charAt(pos); pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } } if (result2 !== null) { if (/^[1-9]/.test(input.charAt(pos))) { result3 = input.charAt(pos); pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("[1-9]"); } } if (result3 !== null) { result2 = [result2, result3]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } if (result2 !== null) { result0 = [result0, result1, result2]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, intPartVal, fracPartVal) { return intPartVal.join('') + "." + fracPartVal.join(''); })(pos0, result0[0], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_integer() { var result0, result1, result2; var pos0, pos1; pos0 = pos; if (input.charCodeAt(pos) === 48) { result0 = "0"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"0\""); } } if (result0 === null) { pos1 = pos; if (/^[1-9]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[1-9]"); } } if (result0 !== null) { result1 = []; if (/^[0-9]/.test(input.charAt(pos))) { result2 = input.charAt(pos); pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } while (result2 !== null) { result1.push(result2); if (/^[0-9]/.test(input.charAt(pos))) { result2 = input.charAt(pos); pos++; } else { result2 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } } if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } if (result0 !== null) { result0 = (function(offset, integerVal) { if (integerVal === "0") { return "0"; } else { return integerVal[0].concat(integerVal[1].join('')); } })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_space() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 32) { result0 = " "; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\" \""); } } if (result0 !== null) { result0 = (function(offset) { return " "; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_circumflex() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 94) { result0 = "^"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"^\""); } } if (result0 !== null) { result0 = (function(offset) { return "^"; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_doubleQuote() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 34) { result0 = "\""; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"\\\"\""); } } if (result0 !== null) { result0 = (function(offset) { return '"'; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_squareBracket() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 91) { result0 = "["; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"[\""); } } if (result0 === null) { if (input.charCodeAt(pos) === 93) { result0 = "]"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"]\""); } } } if (result0 !== null) { result0 = (function(offset, bracketVal) { return bracketVal; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_parentheses() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 40) { result0 = "("; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"(\""); } } if (result0 === null) { if (input.charCodeAt(pos) === 41) { result0 = ")"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\")\""); } } } if (result0 !== null) { result0 = (function(offset, paraVal) { return paraVal; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_comma() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 44) { result0 = ","; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\",\""); } } if (result0 !== null) { result0 = (function(offset) { return ","; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_semicolon() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 59) { result0 = ";"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\";\""); } } if (result0 !== null) { result0 = (function(offset) { return ";"; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_equal() { var result0; var pos0; pos0 = pos; if (input.charCodeAt(pos) === 61) { result0 = "="; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"=\""); } } if (result0 !== null) { result0 = (function(offset) { return "="; })(pos0); } if (result0 === null) { pos = pos0; } return result0; } function parse_character() { var result0; var pos0; pos0 = pos; if (/^[a-z]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[a-z]"); } } if (result0 === null) { if (/^[A-Z]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[A-Z]"); } } if (result0 === null) { if (/^[0-9]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } if (result0 === null) { if (input.charCodeAt(pos) === 45) { result0 = "-"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"-\""); } } if (result0 === null) { if (input.charCodeAt(pos) === 95) { result0 = "_"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"_\""); } } } } } } if (result0 !== null) { result0 = (function(offset, charVal) { return charVal; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function cleanupExpected(expected) { expected.sort(); var lastExpected = null; var cleanExpected = []; for (var i = 0; i < expected.length; i++) { if (expected[i] !== lastExpected) { cleanExpected.push(expected[i]); lastExpected = expected[i]; } } return cleanExpected; } function computeErrorPosition() { /* * The first idea was to use |String.split| to break the input up to the * error position along newlines and derive the line and column from * there. However IE's |split| implementation is so broken that it was * enough to prevent it. */ var line = 1; var column = 1; var seenCR = false; for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) { var ch = input.charAt(i); if (ch === "\n") { if (!seenCR) { line++; } column = 1; seenCR = false; } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { line++; column = 1; seenCR = true; } else { column++; seenCR = false; } } return { line: line, column: column }; } var result = parseFunctions[startRule](); /* * The parser is now in one of the following three states: * * 1. The parser successfully parsed the whole input. * * - |result !== null| * - |pos === input.length| * - |rightmostFailuresExpected| may or may not contain something * * 2. The parser successfully parsed only a part of the input. * * - |result !== null| * - |pos < input.length| * - |rightmostFailuresExpected| may or may not contain something * * 3. The parser did not successfully parse any part of the input. * * - |result === null| * - |pos === 0| * - |rightmostFailuresExpected| contains at least one failure * * All code following this comment (including called functions) must * handle these states. */ if (result === null || pos !== input.length) { var offset = Math.max(pos, rightmostFailuresPos); var found = offset < input.length ? input.charAt(offset) : null; var errorPosition = computeErrorPosition(); throw new this.SyntaxError( cleanupExpected(rightmostFailuresExpected), found, offset, errorPosition.line, errorPosition.column ); } return result; }, /* Returns the parser source code. */ toSource: function() { return this._source; } }; /* Thrown when a parser encounters a syntax error. */ result.SyntaxError = function(expected, found, offset, line, column) { function buildMessage(expected, found) { var expectedHumanized, foundHumanized; switch (expected.length) { case 0: expectedHumanized = "end of input"; break; case 1: expectedHumanized = expected[0]; break; default: expectedHumanized = expected.slice(0, expected.length - 1).join(", ") + " or " + expected[expected.length - 1]; } foundHumanized = found ? quote(found) : "end of input"; return "Expected " + expectedHumanized + " but " + foundHumanized + " found."; } this.name = "SyntaxError"; this.expected = expected; this.found = found; this.message = buildMessage(expected, found); this.offset = offset; this.line = line; this.column = column; }; result.SyntaxError.prototype = Error.prototype; return result; })(); // Description: This model contains the implementation for "instructions" included in the EPUB CFI domain specific language (DSL). // Lexing and parsing a CFI produces a set of executable instructions for processing a CFI (represented in the AST). // This object contains a set of functions that implement each of the executable instructions in the AST. EPUBcfi.CFIInstructions = { // ------------------------------------------------------------------------------------ // // "PUBLIC" METHODS (THE API) // // ------------------------------------------------------------------------------------ // // Description: Follows a step // Rationale: The use of children() is important here, as this jQuery method returns a tree of xml nodes, EXCLUDING // CDATA and text nodes. When we index into the set of child elements, we are assuming that text nodes have been // excluded. // REFACTORING CANDIDATE: This should be called "followIndexStep" getNextNode : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) { // Find the jquery index for the current node var $targetNode; if (CFIStepValue % 2 == 0) { $targetNode = this.elementNodeStep(CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist); } else { $targetNode = this.inferTargetTextNode(CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist); } return $targetNode; }, // Description: This instruction executes an indirection step, where a resource is retrieved using a // link contained on a attribute of the target element. The attribute that contains the link differs // depending on the target. // Note: Iframe indirection will (should) fail if the iframe is not from the same domain as its containing script due to // the cross origin security policy followIndirectionStep : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) { var that = this; var $contentDocument; var $blacklistExcluded; var $startElement; var $targetNode; // TODO: This check must be expanded to all the different types of indirection step // Only expects iframes, at the moment if ($currNode === undefined || !$currNode.is("iframe")) { throw EPUBcfi.NodeTypeError($currNode, "expected an iframe element"); } // Check node type; only iframe indirection is handled, at the moment if ($currNode.is("iframe")) { // Get content $contentDocument = $currNode.contents(); // Go to the first XHTML element, which will be the first child of the top-level document object $blacklistExcluded = this.applyBlacklist($contentDocument.children(), classBlacklist, elementBlacklist, idBlacklist); $startElement = $($blacklistExcluded[0]); // Follow an index step $targetNode = this.getNextNode(CFIStepValue, $startElement, classBlacklist, elementBlacklist, idBlacklist); // Return that shit! return $targetNode; } // TODO: Other types of indirection // TODO: $targetNode.is("embed")) : src // TODO: ($targetNode.is("object")) : data // TODO: ($targetNode.is("image") || $targetNode.is("xlink:href")) : xlink:href }, // Description: Injects an element at the specified text node // Arguments: a cfi text termination string, a jquery object to the current node // REFACTORING CANDIDATE: Rename this to indicate that it injects into a text terminus textTermination : function ($currNode, textOffset, elementToInject) { // Get the first node, this should be a text node if ($currNode === undefined) { throw EPUBcfi.NodeTypeError($currNode, "expected a terminating node, or node list"); } else if ($currNode.length === 0) { throw EPUBcfi.TerminusError("Text", "Text offset:" + textOffset, "no nodes found for termination condition"); } $currNode = this.injectCFIMarkerIntoText($currNode, textOffset, elementToInject); return $currNode; }, // Description: Checks that the id assertion for the node target matches that on // the found node. targetIdMatchesIdAssertion : function ($foundNode, idAssertion) { if ($foundNode.attr("id") === idAssertion) { return true; } else { return false; } }, // ------------------------------------------------------------------------------------ // // "PRIVATE" HELPERS // // ------------------------------------------------------------------------------------ // // Description: Step reference for xml element node. Expected that CFIStepValue is an even integer elementNodeStep : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) { var $targetNode; var $blacklistExcluded; var numElements; var jqueryTargetNodeIndex = (CFIStepValue / 2) - 1; $blacklistExcluded = this.applyBlacklist($currNode.children(), classBlacklist, elementBlacklist, idBlacklist); numElements = $blacklistExcluded.length; if (this.indexOutOfRange(jqueryTargetNodeIndex, numElements)) { throw EPUBcfi.OutOfRangeError(jqueryTargetNodeIndex, numElements - 1, ""); } $targetNode = $($blacklistExcluded[jqueryTargetNodeIndex]); return $targetNode; }, retrieveItemRefHref : function ($itemRefElement, $packageDocument) { return $("#" + $itemRefElement.attr("idref"), $packageDocument).attr("href"); }, indexOutOfRange : function (targetIndex, numChildElements) { return (targetIndex > numChildElements - 1) ? true : false; }, // Rationale: In order to inject an element into a specific position, access to the parent object // is required. This is obtained with the jquery parent() method. An alternative would be to // pass in the parent with a filtered list containing only children that are part of the target text node. injectCFIMarkerIntoText : function ($textNodeList, textOffset, elementToInject) { var nodeNum; var currNodeLength; var currTextPosition = 0; var nodeOffset; var originalText; var $injectedNode; var $newTextNode; // The iteration counter may be incorrect here (should be $textNodeList.length - 1 ??) for (nodeNum = 0; nodeNum <= $textNodeList.length; nodeNum++) { if ($textNodeList[nodeNum].nodeType === 3) { currNodeMaxIndex = ($textNodeList[nodeNum].nodeValue.length - 1) + currTextPosition; nodeOffset = textOffset - currTextPosition; if (currNodeMaxIndex >= textOffset) { // This node is going to be split and the components re-inserted originalText = $textNodeList[nodeNum].nodeValue; // Before part $textNodeList[nodeNum].nodeValue = originalText.slice(0, nodeOffset); // Injected element $injectedNode = $(elementToInject).insertAfter($textNodeList.eq(nodeNum)); // After part $newTextNode = $(document.createTextNode(originalText.slice(nodeOffset, originalText.length))); $($newTextNode).insertAfter($injectedNode); return $textNodeList.parent(); } else { currTextPosition = currTextPosition + currNodeMaxIndex; } } } throw EPUBcfi.TerminusError("Text", "Text offset:" + textOffset, "The offset exceeded the length of the text"); }, // Description: This method finds a target text node and then injects an element into the appropriate node // Arguments: A step value that is an odd integer. A current node with a set of child elements. // Rationale: The possibility that cfi marker elements have been injected into a text node at some point previous to // this method being called (and thus splitting the original text node into two separate text nodes) necessitates that // the set of nodes that compromised the original target text node are inferred and returned. // Notes: Passed a current node. This node should have a set of elements under it. This will include at least one text node, // element nodes (maybe), or possibly a mix. // REFACTORING CANDIDATE: This method is pretty long. Worth investigating to see if it can be refactored into something clearer. inferTargetTextNode : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) { var $elementsWithoutMarkers; var currTextNodePosition; var logicalTargetPosition; var nodeNum; var $targetTextNodeList; // Remove any cfi marker elements from the set of elements. // Rationale: A filtering function is used, as simply using a class selector with jquery appears to // result in behaviour where text nodes are also filtered out, along with the class element being filtered. $elementsWithoutMarkers = this.applyBlacklist($currNode.contents(), classBlacklist, elementBlacklist, idBlacklist); // Convert CFIStepValue to logical index; assumes odd integer for the step value logicalTargetPosition = (parseInt(CFIStepValue) + 1) / 2; // Set text node position counter currTextNodePosition = 1; $targetTextNodeList = $elementsWithoutMarkers.filter( function () { if (currTextNodePosition === logicalTargetPosition) { // If it's a text node if (this.nodeType === 3) { return true; } // Any other type of node, move onto the next text node else { currTextNodePosition++; return false; } } // In this case, don't return any elements else { // If its the last child and it's not a text node, there are no text nodes after it // and the currTextNodePosition shouldn't be incremented if (this.nodeType !== 3 && this !== $elementsWithoutMarkers.lastChild) { currTextNodePosition++; } return false; } } ); // The filtering above should have counted the number of "logical" text nodes; this can be used to // detect out of range errors if ($targetTextNodeList.length === 0) { throw EPUBcfi.OutOfRangeError(logicalTargetPosition, currTextNodePosition - 1, "Index out of range"); } // return the text node list return $targetTextNodeList; }, applyBlacklist : function ($elements, classBlacklist, elementBlacklist, idBlacklist) { var $filteredElements; $filteredElements = $elements.filter( function () { var $currElement = $(this); var includeInList = true; if (classBlacklist) { // Filter each element with the class type $.each(classBlacklist, function (index, value) { if ($currElement.hasClass(value)) { includeInList = false; // Break this loop return false; } }); } if (elementBlacklist) { // For each type of element $.each(elementBlacklist, function (index, value) { if ($currElement.is(value)) { includeInList = false; // Break this loop return false; } }); } if (idBlacklist) { // For each type of element $.each(idBlacklist, function (index, value) { if ($currElement.attr("id") === value) { includeInList = false; // Break this loop return false; } }); } return includeInList; } ); return $filteredElements; } }; // Description: This is an interpreter that inteprets an Abstract Syntax Tree (AST) for a CFI. The result of executing the interpreter // is to inject an element, or set of elements, into an EPUB content document (which is just an XHTML document). These element(s) will // represent the position or area in the EPUB referenced by a CFI. // Rationale: The AST is a clean and readable expression of the step-terminus structure of a CFI. Although building an interpreter adds to the // CFI infrastructure, it provides a number of benefits. First, it emphasizes a clear separation of concerns between lexing/parsing a // CFI, which involves some complexity related to escaped and special characters, and the execution of the underlying set of steps // represented by the CFI. Second, it will be easier to extend the interpreter to account for new/altered CFI steps (say for references // to vector objects or multiple CFIs) than if lexing, parsing and interpretation were all handled in a single step. Finally, Readium's objective is // to demonstrate implementation of the EPUB 3.0 spec. An implementation with a strong separation of concerns that conforms to // well-understood patterns for DSL processing should be easier to communicate, analyze and understand. // REFACTORING CANDIDATE: node type errors shouldn't really be possible if the CFI syntax is correct and the parser is error free. // Might want to make the script die in those instances, once the grammar and interpreter are more stable. // REFACTORING CANDIDATE: The use of the 'nodeType' property is confusing as this is a DOM node property and the two are unrelated. // Whoops. There shouldn't be any interference, however, I think this should be changed. EPUBcfi.Interpreter = { // ------------------------------------------------------------------------------------ // // "PUBLIC" METHODS (THE API) // // ------------------------------------------------------------------------------------ // // Description: Find the content document referenced by the spine item. This should be the spine item // referenced by the first indirection step in the CFI. // Rationale: This method is a part of the API so that the reading system can "interact" the content document // pointed to by a CFI. If this is not a separate step, the processing of the CFI must be tightly coupled with // the reading system, as it stands now. getContentDocHref : function (CFI, packageDocument, classBlacklist, elementBlacklist, idBlacklist) { // Decode for URI/IRI escape characters var $packageDocument = $(packageDocument); var decodedCFI = decodeURI(CFI); var CFIAST = EPUBcfi.Parser.parse(decodedCFI); // Check node type; throw error if wrong type if (CFIAST === undefined || CFIAST.type !== "CFIAST") { throw EPUBcfi.NodeTypeError(CFIAST, "expected CFI AST root node"); } var $packageElement = $($("package", $packageDocument)[0]); // Interpet the path node (the package document step) var $currElement = this.interpretIndexStepNode(CFIAST.cfiString.path, $packageElement, classBlacklist, elementBlacklist, idBlacklist); // Interpret the local_path node, which is a set of steps and and a terminus condition var stepNum = 0; var nextStepNode; for (stepNum = 0 ; stepNum <= CFIAST.cfiString.localPath.steps.length - 1 ; stepNum++) { nextStepNode = CFIAST.cfiString.localPath.steps[stepNum]; if (nextStepNode.type === "indexStep") { $currElement = this.interpretIndexStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist); } else if (nextStepNode.type === "indirectionStep") { $currElement = this.interpretIndirectionStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist); } // Found the content document href referenced by the spine item if ($currElement.is("itemref")) { return EPUBcfi.CFIInstructions.retrieveItemRefHref($currElement, $packageDocument); } } // TODO: If you get to here, an itemref element was never found - a runtime error. The cfi is misspecified or // the package document is messed up. }, // Description: Inject an arbitrary html element into a position in a content document referenced by a CFI injectElement : function (CFI, contentDocument, elementToInject, classBlacklist, elementBlacklist, idBlacklist) { var decodedCFI = decodeURI(CFI); var CFIAST = EPUBcfi.Parser.parse(decodedCFI); var indirectionNode; var indirectionStepNum; // Rationale: Since the correct content document for this CFI is already being passed, we can skip to the beginning // of the indirection step that referenced the content document. // Note: This assumes that indirection steps and index steps conform to an interface: an object with stepLength, idAssertion indirectionStepNum = this.getFirstIndirectionStepNum(CFIAST); indirectionNode = CFIAST.cfiString.localPath.steps[indirectionStepNum]; indirectionNode.type = "indexStep"; // Interpret the rest of the steps $currElement = this.interpretLocalPath(CFIAST.cfiString, indirectionStepNum, $("html", contentDocument), classBlacklist, elementBlacklist, idBlacklist); // TODO: detect what kind of terminus; for now, text node termini are the only kind implemented $currElement = this.interpretTextTerminusNode(CFIAST.cfiString.localPath.termStep, $currElement, elementToInject); // Return the element that was injected into return $currElement; }, // Description: This method will return the element or node (say, a text node) that is the final target of the // the CFI. getTargetElement : function (CFI, contentDocument, classBlacklist, elementBlacklist, idBlacklist) { var decodedCFI = decodeURI(CFI); var CFIAST = EPUBcfi.Parser.parse(decodedCFI); var indirectionNode; var indirectionStepNum; // Rationale: Since the correct content document for this CFI is already being passed, we can skip to the beginning // of the indirection step that referenced the content document. // Note: This assumes that indirection steps and index steps conform to an interface: an object with stepLength, idAssertion indirectionStepNum = this.getFirstIndirectionStepNum(CFIAST); indirectionNode = CFIAST.cfiString.localPath.steps[indirectionStepNum]; indirectionNode.type = "indexStep"; // Interpret the rest of the steps $currElement = this.interpretLocalPath(CFIAST.cfiString, indirectionStepNum, $("html", contentDocument), classBlacklist, elementBlacklist, idBlacklist); // Return the element at the end of the CFI return $currElement; }, // Description: This method allows a "partial" CFI to be used to reference a target in a content document, without a // package document CFI component. // Arguments: { // contentDocumentCFI : This is a partial CFI that represents a path in a content document only. This partial must be // syntactically valid, even though it references a path starting at the top of a content document (which is a CFI that // that has no defined meaning in the spec.) // contentDocument : A DOM representation of the content document to which the partial CFI refers. // } // Rationale: This method exists to meet the requirements of the Readium-SDK and should be used with care getTargetElementWithPartialCFI : function (contentDocumentCFI, contentDocument, classBlacklist, elementBlacklist, idBlacklist) { var decodedCFI = decodeURI(contentDocumentCFI); var CFIAST = EPUBcfi.Parser.parse(decodedCFI); var indirectionNode; // Interpret the path node var $currElement = this.interpretIndexStepNode(CFIAST.cfiString.path, $("html", contentDocument), classBlacklist, elementBlacklist, idBlacklist); // Interpret the rest of the steps $currElement = this.interpretLocalPath(CFIAST.cfiString, 0, $currElement, classBlacklist, elementBlacklist, idBlacklist); // Return the element at the end of the CFI return $currElement; }, // Description: This method allows a "partial" CFI to be used, with a content document, to return the text node and offset // referenced by the partial CFI. // Arguments: { // contentDocumentCFI : This is a partial CFI that represents a path in a content document only. This partial must be // syntactically valid, even though it references a path starting at the top of a content document (which is a CFI that // that has no defined meaning in the spec.) // contentDocument : A DOM representation of the content document to which the partial CFI refers. // } // Rationale: This method exists to meet the requirements of the Readium-SDK and should be used with care getTextTerminusInfoWithPartialCFI : function (contentDocumentCFI, contentDocument, classBlacklist, elementBlacklist, idBlacklist) { var decodedCFI = decodeURI(contentDocumentCFI); var CFIAST = EPUBcfi.Parser.parse(decodedCFI); var indirectionNode; var textOffset; // Interpret the path node var $currElement = this.interpretIndexStepNode(CFIAST.cfiString.path, $("html", contentDocument), classBlacklist, elementBlacklist, idBlacklist); // Interpret the rest of the steps $currElement = this.interpretLocalPath(CFIAST.cfiString, 0, $currElement, classBlacklist, elementBlacklist, idBlacklist); // Return the element at the end of the CFI textOffset = parseInt(CFIAST.cfiString.localPath.termStep.offsetValue); return { textNode : $currElement, textOffset : textOffset }; }, // ------------------------------------------------------------------------------------ // // "PRIVATE" HELPERS // // ------------------------------------------------------------------------------------ // getFirstIndirectionStepNum : function (CFIAST) { // Find the first indirection step in the local path; follow it like a regular step, as the step in the content document it // references is already loaded and has been passed to this method var stepNum = 0; for (stepNum; stepNum <= CFIAST.cfiString.localPath.steps.length - 1 ; stepNum++) { nextStepNode = CFIAST.cfiString.localPath.steps[stepNum]; if (nextStepNode.type === "indirectionStep") { return stepNum; } } }, // REFACTORING CANDIDATE: cfiString node and start step num could be merged into one argument, by simply passing the // starting step. interpretLocalPath : function (cfiStringNode, startStepNum, $currElement, classBlacklist, elementBlacklist, idBlacklist) { var stepNum = startStepNum; var nextStepNode; for (stepNum; stepNum <= cfiStringNode.localPath.steps.length - 1 ; stepNum++) { nextStepNode = cfiStringNode.localPath.steps[stepNum]; if (nextStepNode.type === "indexStep") { $currElement = this.interpretIndexStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist); } else if (nextStepNode.type === "indirectionStep") { $currElement = this.interpretIndirectionStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist); } } return $currElement; }, interpretIndexStepNode : function (indexStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist) { // Check node type; throw error if wrong type if (indexStepNode === undefined || indexStepNode.type !== "indexStep") { throw EPUBcfi.NodeTypeError(indexStepNode, "expected index step node"); } // Index step var $stepTarget = EPUBcfi.CFIInstructions.getNextNode(indexStepNode.stepLength, $currElement, classBlacklist, elementBlacklist, idBlacklist); // Check the id assertion, if it exists if (indexStepNode.idAssertion) { if (!EPUBcfi.CFIInstructions.targetIdMatchesIdAssertion($stepTarget, indexStepNode.idAssertion)) { throw EPUBcfi.CFIAssertionError(indexStepNode.idAssertion, $stepTarget.attr('id'), "Id assertion failed"); } } return $stepTarget; }, interpretIndirectionStepNode : function (indirectionStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist) { // Check node type; throw error if wrong type if (indirectionStepNode === undefined || indirectionStepNode.type !== "indirectionStep") { throw EPUBcfi.NodeTypeError(indirectionStepNode, "expected indirection step node"); } // Indirection step var $stepTarget = EPUBcfi.CFIInstructions.followIndirectionStep( indirectionStepNode.stepLength, $currElement, classBlacklist, elementBlacklist); // Check the id assertion, if it exists if (indirectionStepNode.idAssertion) { if (!EPUBcfi.CFIInstructions.targetIdMatchesIdAssertion($stepTarget, indirectionStepNode.idAssertion)) { throw EPUBcfi.CFIAssertionError(indirectionStepNode.idAssertion, $stepTarget.attr('id'), "Id assertion failed"); } } return $stepTarget; }, // REFACTORING CANDIDATE: The logic here assumes that a user will always want to use this terminus // to inject content into the found node. This should be changed to be more flexible. interpretTextTerminusNode : function (terminusNode, $currElement, elementToInject) { if (terminusNode === undefined || terminusNode.type !== "textTerminus") { throw EPUBcfi.NodeTypeError(terminusNode, "expected text terminus node"); } var $elementInjectedInto = EPUBcfi.CFIInstructions.textTermination( $currElement, terminusNode.offsetValue, elementToInject); return $elementInjectedInto; } }; // Description: This is a set of runtime errors that the CFI interpreter can throw. // Rationale: These error types extend the basic javascript error object so error things like the stack trace are // included with the runtime errors. // REFACTORING CANDIDATE: This type of error may not be required in the long run. The parser should catch any syntax errors, // provided it is error-free, and as such, the AST should never really have any node type errors, which are essentially errors // in the structure of the AST. This error should probably be refactored out when the grammar and interpreter are more stable. EPUBcfi.NodeTypeError = function (node, message) { function NodeTypeError () { this.node = node; } NodeTypeError.prototype = new Error(message); NodeTypeError.constructor = NodeTypeError; return new NodeTypeError(); }; // REFACTORING CANDIDATE: Might make sense to include some more specifics about the out-of-rangeyness. EPUBcfi.OutOfRangeError = function (targetIndex, maxIndex, message) { function OutOfRangeError () { this.targetIndex = targetIndex; this.maxIndex = maxIndex; } OutOfRangeError.prototype = new Error(message); OutOfRangeError.constructor = OutOfRangeError() return new OutOfRangeError(); }; // REFACTORING CANDIDATE: This is a bit too general to be useful. When I have a better understanding of the type of errors // that can occur with the various terminus conditions, it'll make more sense to revisit this. EPUBcfi.TerminusError = function (terminusType, terminusCondition, message) { function TerminusError () { this.terminusType = terminusType; this.terminusCondition = terminusCondition; } TerminusError.prototype = new Error(message); TerminusError.constructor = TerminusError(); return new TerminusError(); }; EPUBcfi.CFIAssertionError = function (expectedAssertion, targetElementAssertion, message) { function CFIAssertionError () { this.expectedAssertion = expectedAssertion; this.targetElementAssertion = targetElementAssertion; } CFIAssertionError.prototype = new Error(message); CFIAssertionError.constructor = CFIAssertionError(); return new CFIAssertionError(); }; EPUBcfi.Generator = { // ------------------------------------------------------------------------------------ // // "PUBLIC" METHODS (THE API) // // ------------------------------------------------------------------------------------ // // Description: Generates a character offset CFI // Arguments: The text node that contains the offset referenced by the cfi, the offset value, the name of the // content document that contains the text node, the package document for this EPUB. generateCharacterOffsetCFIComponent : function (startTextNode, characterOffset, classBlacklist, elementBlacklist, idBlacklist) { var textNodeStep; var contentDocCFI; var $itemRefStartNode; var packageDocCFI; this.validateStartTextNode(startTextNode, characterOffset); // Create the text node step textNodeStep = this.createCFITextNodeStep($(startTextNode), characterOffset, classBlacklist, elementBlacklist, idBlacklist); // Call the recursive method to create all the steps up to the head element of the content document (the "html" element) contentDocCFI = this.createCFIElementSteps($(startTextNode).parent(), "html", classBlacklist, elementBlacklist, idBlacklist) + textNodeStep; return contentDocCFI.substring(1, contentDocCFI.length); }, generateElementCFIComponent : function (startElement, classBlacklist, elementBlacklist, idBlacklist) { var contentDocCFI; var $itemRefStartNode; var packageDocCFI; // Call the recursive method to create all the steps up to the head element of the content document (the "html" element) contentDocCFI = this.createCFIElementSteps($(startElement), "html", classBlacklist, elementBlacklist, idBlacklist); // Remove the ! return contentDocCFI.substring(1, contentDocCFI.length); }, generatePackageDocumentCFIComponent : function (contentDocumentName, packageDocument, classBlacklist, elementBlacklist, idBlacklist) { this.validateContentDocumentName(contentDocumentName); this.validatePackageDocument(packageDocument, contentDocumentName); // Get the start node (itemref element) that references the content document $itemRefStartNode = $("itemref[idref='" + contentDocumentName + "']", $(packageDocument)); // Create the steps up to the top element of the package document (the "package" element) packageDocCFIComponent = this.createCFIElementSteps($itemRefStartNode, "package", classBlacklist, elementBlacklist, idBlacklist); // Append an !; this assumes that a CFI content document CFI component will be appended at some point return packageDocCFIComponent + "!"; }, generateCompleteCFI : function (packageDocumentCFIComponent, contentDocumentCFIComponent) { return "epubcfi(" + packageDocumentCFIComponent + contentDocumentCFIComponent + ")"; }, // ------------------------------------------------------------------------------------ // // "PRIVATE" HELPERS // // ------------------------------------------------------------------------------------ // validateStartTextNode : function (startTextNode, characterOffset) { // Check that the text node to start from IS a text node if (!startTextNode) { throw new EPUBcfi.NodeTypeError(startTextNode, "Cannot generate a character offset from a starting point that is not a text node"); } else if (startTextNode.nodeType != 3) { throw new EPUBcfi.NodeTypeError(startTextNode, "Cannot generate a character offset from a starting point that is not a text node"); } // Check that the character offset is within a valid range for the text node supplied if (characterOffset < 0) { throw new EPUBcfi.OutOfRangeError(characterOffset, 0, "Character offset cannot be less than 0"); } else if (characterOffset > startTextNode.nodeValue.length) { throw new EPUBcfi.OutOfRangeError(characterOffset, startTextNode.nodeValue.length - 1, "character offset cannot be greater than the length of the text node"); } }, validateContentDocumentName : function (contentDocumentName) { // Check that the idref for the content document has been provided if (!contentDocumentName) { throw new Error("The idref for the content document, as found in the spine, must be supplied"); } }, validatePackageDocument : function (packageDocument, contentDocumentName) { // Check that the package document is non-empty and contains an itemref element for the supplied idref if (!packageDocument) { throw new Error("A package document must be supplied to generate a CFI"); } else if ($($("itemref[idref='" + contentDocumentName + "']", packageDocument)[0]).length === 0) { throw new Error("The idref of the content document could not be found in the spine"); } }, // Description: Creates a CFI terminating step, to a text node, with a character offset // REFACTORING CANDIDATE: Some of the parts of this method could be refactored into their own methods createCFITextNodeStep : function ($startTextNode, characterOffset, classBlacklist, elementBlacklist, idBlacklist) { var $parentNode; var $contentsExcludingMarkers; var CFIIndex; var indexOfTextNode; var preAssertion; var preAssertionStartIndex; var textLength; var postAssertion; var postAssertionEndIndex; // Find text node position in the set of child elements, ignoring any blacklisted elements $parentNode = $startTextNode.parent(); $contentsExcludingMarkers = EPUBcfi.CFIInstructions.applyBlacklist($parentNode.contents(), classBlacklist, elementBlacklist, idBlacklist); // Find the text node index in the parent list, inferring nodes that were originally a single text node var prevNodeWasTextNode; var indexOfFirstInSequence; $.each($contentsExcludingMarkers, function (index) { // If this is a text node, check if it matches and return the current index if (this.nodeType === 3) { if (this === $startTextNode[0]) { // Set index as the first in the adjacent sequence of text nodes, or as the index of the current node if this // node is a standard one sandwiched between two element nodes. if (prevNodeWasTextNode) { indexOfTextNode = indexOfFirstInSequence; } else { indexOfTextNode = index; } // Break out of .each loop return false; } // Save this index as the first in sequence of adjacent text nodes, if it is not already set by this point prevNodeWasTextNode = true; if (!indexOfFirstInSequence) { indexOfFirstInSequence = index; } } // This node is not a text node else { prevNodeWasTextNode = false; indexOfFirstInSequence = undefined; } } ); // Convert the text node index to a CFI odd-integer representation CFIIndex = (indexOfTextNode * 2) + 1; // TODO: text assertions are not in the grammar yet, I think, or they're just causing problems. This has // been temporarily removed. // Add pre- and post- text assertions // preAssertionStartIndex = (characterOffset - 3 >= 0) ? characterOffset - 3 : 0; // preAssertion = $startTextNode[0].nodeValue.substring(preAssertionStartIndex, characterOffset); // textLength = $startTextNode[0].nodeValue.length; // postAssertionEndIndex = (characterOffset + 3 <= textLength) ? characterOffset + 3 : textLength; // postAssertion = $startTextNode[0].nodeValue.substring(characterOffset, postAssertionEndIndex); // Gotta infer the correct character offset, as well // Return the constructed CFI text node step return "/" + CFIIndex + ":" + characterOffset; // + "[" + preAssertion + "," + postAssertion + "]"; }, // Description: A set of adjacent text nodes can be inferred to have been a single text node in the original document. As such, // if the character offset is specified for one of the adjacent text nodes, the true offset for the original node must be // inferred. findOriginalTextNodeCharOffset : function ($startTextNode, specifiedCharacterOffset, classBlacklist, elementBlacklist, idBlacklist) { var $parentNode; var $contentsExcludingMarkers; var textLength; // Find text node position in the set of child elements, ignoring any cfi markers $parentNode = $startTextNode.parent(); $contentsExcludingMarkers = EPUBcfi.CFIInstructions.applyBlacklist($parentNode.contents(), classBlacklist, elementBlacklist, idBlacklist); // Find the text node number in the list, inferring nodes that were originally a single text node var prevNodeWasTextNode; var originalCharOffset = -1; // So the character offset is a 0-based index; we'll be adding lengths of text nodes to this number $.each($contentsExcludingMarkers, function (index) { // If this is a text node, check if it matches and return the current index if (this.nodeType === 3) { if (this === $startTextNode[0]) { if (prevNodeWasTextNode) { originalCharOffset = originalCharOffset + specifiedCharacterOffset; } else { originalCharOffset = specifiedCharacterOffset; } return false; // Break out of .each loop } else { originalCharOffset = originalCharOffset + this.length; } // save this index as the first in sequence of adjacent text nodes, if not set prevNodeWasTextNode = true; } // This node is not a text node else { prevNodeWasTextNode = false; } } ); return originalCharOffset; }, createCFIElementSteps : function ($currNode, topLevelElement, classBlacklist, elementBlacklist, idBlacklist) { var $blacklistExcluded; var $parentNode; var currNodePosition; var CFIPosition; var idAssertion; var elementStep; // Find position of current node in parent list $blacklistExcluded = EPUBcfi.CFIInstructions.applyBlacklist($currNode.parent().children(), classBlacklist, elementBlacklist, idBlacklist); $.each($blacklistExcluded, function (index, value) { if (this === $currNode[0]) { currNodePosition = index; // Break loop return false; } }); // Convert position to the CFI even-integer representation CFIPosition = (currNodePosition + 1) * 2; // Create CFI step with id assertion, if the element has an id if ($currNode.attr("id")) { elementStep = "/" + CFIPosition + "[" + $currNode.attr("id") + "]"; } else { elementStep = "/" + CFIPosition; } // If a parent is an html element return the (last) step for this content document, otherwise, continue. // Also need to check if the current node is the top-level element. This can occur if the start node is also the // top level element. $parentNode = $currNode.parent(); if ($parentNode.is(topLevelElement) || $currNode.is(topLevelElement)) { // If the top level node is a type from which an indirection step, add an indirection step character (!) // REFACTORING CANDIDATE: It is possible that this should be changed to: if (topLevelElement = 'package') do // not return an indirection character. Every other type of top-level element may require an indirection // step to navigate to, thus requiring that ! is always prepended. if (topLevelElement === 'html') { return "!" + elementStep; } else { return elementStep; } } else { return this.createCFIElementSteps($parentNode, topLevelElement, classBlacklist, elementBlacklist, idBlacklist) + elementStep; } } }; if (global.EPUBcfi) { throw new Error('The EPUB cfi library has already been defined'); } else { global.EPUBcfi = EPUBcfi; } }) (typeof window === 'undefined' ? this : window); define("epubCfi", function(){}); // LauncherOSX // // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * CFI navigation helper class * * @param $viewport * @param $iframe * @constructor */ ReadiumSDK.Views.CfiNavigationLogic = function($viewport, $iframe){ this.$viewport = $viewport; this.$iframe = $iframe; this.getRootElement = function(){ return this.$iframe[0].contentDocument.documentElement }; //we look for text and images this.findFirstVisibleElement = function (topOffset) { var $elements; var $firstVisibleTextNode = null; var percentOfElementHeight = 0; $elements = $("body", this.getRootElement()).find(":not(iframe)").contents().filter(function () { return this.nodeType === Node.TEXT_NODE || this.nodeName.toLowerCase() === 'img'; }); // Find the first visible text node $.each($elements, function() { var $element; if(this.nodeType === Node.TEXT_NODE) { //text node // Heuristic to find a text node with actual text var nodeText = this.nodeValue.replace(/\n/g, ""); nodeText = nodeText.replace(/ /g, ""); if(nodeText.length > 0) { $element = $(this).parent(); } else { return true; //next element } } else { $element = $(this); //image } var elementRect = ReadiumSDK.Helpers.Rect.fromElement($element); if (elementRect.bottom() > topOffset) { $firstVisibleTextNode = $element; if(elementRect.top > topOffset) { percentOfElementHeight = 0; } else { percentOfElementHeight = Math.ceil(((topOffset - elementRect.top) / elementRect.height) * 100); } // Break the loop return false; } return true; //next element }); return {$element: $firstVisibleTextNode, percentY: percentOfElementHeight}; }; this.getFirstVisibleElementCfi = function(topOffset) { var foundElement = this.findFirstVisibleElement(topOffset); if(!foundElement.$element) { console.log("Could not generate CFI no visible element on page"); return undefined; } var cfi = EPUBcfi.Generator.generateElementCFIComponent(foundElement.$element[0]); if(cfi[0] == "!") { cfi = cfi.substring(1); } return cfi + "@0:" + foundElement.percentY; }; this.getPageForElementCfi = function(cfi) { var contentDoc = this.$iframe[0].contentDocument; var cfiParts = this.splitCfi(cfi); var wrappedCfi = "epubcfi(" + cfiParts.cfi + ")"; var $element = EPUBcfi.Interpreter.getTargetElementWithPartialCFI(wrappedCfi, contentDoc); if(!$element || $element.length == 0) { console.log("Can't find element for CFI: " + cfi); return undefined; } return this.getPageForElement($element, cfiParts.x, cfiParts.y); }; this.getPageForElement = function($element, x, y) { var elementRect = ReadiumSDK.Helpers.Rect.fromElement($element); var posInElement = Math.ceil(elementRect.top + y * elementRect.height / 100); var column = Math.floor(posInElement / this.$viewport.height()); return column; }; this.getPageForElementId = function(id) { var contentDoc = this.$iframe[0].contentDocument; var $element = $("#" + id, contentDoc); if($element.length == 0) { return -1; } return this.getPageForElement($element, 0, 0); }; this.splitCfi = function(cfi) { var ret = { cfi: "", x: 0, y: 0 }; var ix = cfi.indexOf("@"); if(ix != -1) { var terminus = cfi.substring(ix + 1); var colIx = terminus.indexOf(":"); if(colIx != -1) { ret.x = parseInt(terminus.substr(0, colIx)); ret.y = parseInt(terminus.substr(colIx + 1)); } else { console.log("Unexpected terminating step format"); } ret.cfi = cfi.substring(0, ix); } else { ret.cfi = cfi; } return ret; }; }; define("cfiNavigationLogic", function(){}); // LauncherOSX // // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Renders reflowable content using CSS columns * * @class ReadiumSDK.Views.ReflowableView */ ReadiumSDK.Views.ReflowableView = Backbone.View.extend({ currentSpineItem: undefined, isWaitingFrameRender: false, deferredPageRequest: undefined, spine: undefined, fontSize:100, lastViewPortSize : { width: undefined, height: undefined }, paginationInfo : { visibleColumnCount : 2, columnGap : 20, spreadCount : 0, currentSpreadIndex : 0, columnWidth : undefined, pageOffset : 0, columnCount: 0 }, initialize: function() { this.spine = this.options.spine; }, render: function(){ this.template = _.template($("#template-reflowable-view").html(), {}); this.setElement(this.template); this.$iframe = $("#epubContentIframe", this.$el); this.$iframe.css("left", ""); this.$iframe.css("right", ""); this.$iframe.css(this.spine.isLeftToRight() ? "left" : "right", "0px"); //We will call onViewportResize after user stopped resizing window var lazyResize = _.debounce(this.onViewportResize, 100); $(window).on("resize.ReadiumSDK.reflowableView", _.bind(lazyResize, this)); return this; }, remove: function() { $(window).off("resize.ReadiumSDK.reflowableView"); //base remove Backbone.View.prototype.remove.call(this); }, isReflowable: function() { return true; }, onViewportResize: function() { if(this.updateViewportSize()) { this.updatePagination(); } }, setViewSettings: function(settings) { this.paginationInfo.visibleColumnCount = settings.isSyntheticSpread ? 2 : 1; this.paginationInfo.columnGap = settings.columnGap; this.fontSize = settings.fontSize; this.updateHtmlFontSizeAndColumnGap(); this.updatePagination(); }, registerTriggers: function (doc) { $('trigger', doc).each(function() { var trigger = new ReadiumSDK.Models.Trigger(this); trigger.subscribe(doc); }); }, loadSpineItem: function(spineItem) { if(this.currentSpineItem != spineItem) { this.paginationInfo.currentSpreadIndex = 0; this.currentSpineItem = spineItem; this.isWaitingFrameRender = true; var src = this.spine.getItemUrl(spineItem); ReadiumSDK.Helpers.LoadIframe(this.$iframe[0], src, this.onIFrameLoad, this); } }, updateHtmlFontSizeAndColumnGap: function() { if(this.$epubHtml) { this.$epubHtml.css("font-size", this.fontSize + "%"); this.$epubHtml.css("-webkit-column-gap", this.paginationInfo.columnGap + "px"); } }, onIFrameLoad : function(success) { this.isWaitingFrameRender = false; //while we where loading frame new request came if(this.deferredPageRequest && this.deferredPageRequest.spineItem != this.currentSpineItem) { this.loadSpineItem(this.deferredPageRequest.spineItem); return; } if(!success) { this.deferredPageRequest = undefined; return; } var epubContentDocument = this.$iframe[0].contentDocument; this.$epubHtml = $("html", epubContentDocument); this.$epubHtml.css("height", "100%"); this.$epubHtml.css("position", "absolute"); this.$epubHtml.css("-webkit-column-axis", "horizontal"); this.updateHtmlFontSizeAndColumnGap(); ///////// //Columns Debugging // $epubHtml.css("-webkit-column-rule-color", "red"); // $epubHtml.css("-webkit-column-rule-style", "dashed"); // $epubHtml.css("background-color", '#b0c4de'); ///////// this.updateViewportSize(); this.updatePagination(); this.applySwitches(epubContentDocument); this.registerTriggers(epubContentDocument); }, openDeferredElement: function() { if(!this.deferredPageRequest) { return; } var deferredData = this.deferredPageRequest; this.deferredPageRequest = undefined; this.openPage(deferredData); }, openPage: function(pageRequest) { if(this.isWaitingFrameRender) { this.deferredPageRequest = pageRequest; return; } // if no spine item specified we are talking about current spine item if(pageRequest.spineItem && pageRequest.spineItem != this.currentSpineItem) { this.deferredPageRequest = pageRequest; this.loadSpineItem(pageRequest.spineItem); return; } var pageIndex = undefined; var navigation = new ReadiumSDK.Views.CfiNavigationLogic(this.$el, this.$iframe); if(pageRequest.spineItemPageIndex !== undefined) { pageIndex = pageRequest.spineItemPageIndex; } else if(pageRequest.elementId) { pageIndex = navigation.getPageForElementId(pageRequest.elementId); } else if(pageRequest.elementCfi) { pageIndex = navigation.getPageForElementCfi(pageRequest.elementCfi); } else if(pageRequest.firstPage) { pageIndex = 0; } else if(pageRequest.lastPage) { pageIndex = this.paginationInfo.columnCount - 1; } if(pageIndex !== undefined && pageIndex >= 0 && pageIndex < this.paginationInfo.columnCount) { this.paginationInfo.currentSpreadIndex = Math.floor(pageIndex / this.paginationInfo.visibleColumnCount) ; this.onPaginationChanged(); } }, redraw: function() { var offsetVal = -this.paginationInfo.pageOffset + "px"; this.$epubHtml.css("left", this.spine.isLeftToRight() ? offsetVal : ""); this.$epubHtml.css("right", this.spine.isRightToLeft() ? offsetVal : ""); }, updateViewportSize: function() { var newWidth = this.$el.width(); var newHeight = this.$el.height(); if(this.lastViewPortSize.width !== newWidth || this.lastViewPortSize.height !== newHeight){ this.lastViewPortSize.width = newWidth; this.lastViewPortSize.height = newHeight; return true; } return false; }, // Description: Parse the epub "switch" tags and hide // cases that are not supported applySwitches: function(dom) { // helper method, returns true if a given case node // is supported, false otherwise var isSupported = function(caseNode) { var ns = caseNode.attributes["required-namespace"]; if(!ns) { // the namespace was not specified, that should // never happen, we don't support it then console.log("Encountered a case statement with no required-namespace"); return false; } // all the xmlns that readium is known to support // TODO this is going to require maintenance var supportedNamespaces = ["http://www.w3.org/1998/Math/MathML"]; return _.include(supportedNamespaces, ns); }; $('switch', dom).each( function() { // keep track of whether or now we found one var found = false; $('case', this).each(function() { if( !found && isSupported(this) ) { found = true; // we found the node, don't remove it } else { $(this).remove(); // remove the node from the dom // $(this).prop("hidden", true); } }); if(found) { // if we found a supported case, remove the default $('default', this).remove(); // $('default', this).prop("hidden", true); } }) }, onPaginationChanged: function() { this.paginationInfo.pageOffset = (this.paginationInfo.columnWidth + this.paginationInfo.columnGap) * this.paginationInfo.visibleColumnCount * this.paginationInfo.currentSpreadIndex; this.redraw(); this.trigger("ViewPaginationChanged"); }, openPagePrev: function () { if(!this.currentSpineItem) { return; } if(this.paginationInfo.currentSpreadIndex > 0) { this.paginationInfo.currentSpreadIndex--; this.onPaginationChanged(); } else { var prevSpineItem = this.spine.prevItem(this.currentSpineItem); if(prevSpineItem) { var pageRequest = new ReadiumSDK.Models.PageOpenRequest(prevSpineItem); pageRequest.setLastPage(); this.openPage(pageRequest); } } }, openPageNext: function () { if(!this.currentSpineItem) { return; } if(this.paginationInfo.currentSpreadIndex < this.paginationInfo.spreadCount - 1) { this.paginationInfo.currentSpreadIndex++; this.onPaginationChanged(); } else { var nextSpineItem = this.spine.nextItem(this.currentSpineItem); if(nextSpineItem) { var pageRequest = new ReadiumSDK.Models.PageOpenRequest(nextSpineItem); pageRequest.setFirstPage(); this.openPage(pageRequest); } } }, updatePagination: function() { if(!this.$epubHtml) { return; } this.$iframe.css("width", this.lastViewPortSize.width + "px"); this.$iframe.css("height", this.lastViewPortSize.height + "px"); this.$epubHtml.css("height", this.lastViewPortSize.height + "px"); this.paginationInfo.columnWidth = (this.lastViewPortSize.width - this.paginationInfo.columnGap * (this.paginationInfo.visibleColumnCount - 1)) / this.paginationInfo.visibleColumnCount; //we do this because CSS will floor column with by itself if it is not a round number this.paginationInfo.columnWidth = Math.floor(this.paginationInfo.columnWidth); this.$epubHtml.css("width", this.paginationInfo.columnWidth); this.shiftBookOfScreen(); this.$epubHtml.css("-webkit-column-width", this.paginationInfo.columnWidth + "px"); var self = this; //TODO it takes time for rendition_layout engine to arrange columns we waite //it would be better to react on rendition_layout column reflow finished event setTimeout(function(){ var columnizedContentWidth = self.$epubHtml[0].scrollWidth; self.paginationInfo.columnCount = Math.round((columnizedContentWidth + self.paginationInfo.columnGap) / (self.paginationInfo.columnWidth + self.paginationInfo.columnGap)); self.paginationInfo.spreadCount = Math.ceil(self.paginationInfo.columnCount / self.paginationInfo.visibleColumnCount); if(self.paginationInfo.currentSpreadIndex >= self.paginationInfo.spreadCount) { self.paginationInfo.currentSpreadIndex = self.paginationInfo.spreadCount - 1; } self.openDeferredElement(); //We do this to force re-rendering of the document in the iframe. //There is a bug in WebView control with right to left columns layout - after resizing the window html document //is shifted in side the containing div. Hiding and showing the html element puts document in place. self.$epubHtml.hide(); setTimeout(function() { self.$epubHtml.show(); self.onPaginationChanged(); }, 50); }, 100); }, shiftBookOfScreen: function() { if(this.spine.isLeftToRight()) { this.$epubHtml.css("left", (this.lastViewPortSize.width + 1000) + "px"); } else { this.$epubHtml.css("right", (this.lastViewPortSize.width + 1000) + "px"); } }, getFirstVisibleElementCfi: function(){ var columnsLeftOfViewport = Math.round(this.paginationInfo.pageOffset / (this.paginationInfo.columnWidth + this.paginationInfo.columnGap)); var topOffset = columnsLeftOfViewport * this.$el.height(); var navigation = new ReadiumSDK.Views.CfiNavigationLogic(this.$el, this.$iframe); return navigation.getFirstVisibleElementCfi(topOffset); }, getPaginationInfo: function() { var paginationInfo = new ReadiumSDK.Models.CurrentPagesInfo(this.spine.items.length, this.spine.package.isFixedLayout(), this.spine.direction); if(!this.currentSpineItem) { return paginationInfo; } var currentPage = this.paginationInfo.currentSpreadIndex * this.paginationInfo.visibleColumnCount; for(var i = 0; i < this.paginationInfo.visibleColumnCount && (currentPage + i) < this.paginationInfo.columnCount; i++) { paginationInfo.addOpenPage(currentPage + i, this.paginationInfo.columnCount, this.currentSpineItem.idref, this.currentSpineItem.index); } return paginationInfo; }, bookmarkCurrentPage: function() { if(!this.currentSpineItem) { return new ReadiumSDK.Models.BookmarkData("", ""); } return new ReadiumSDK.Models.BookmarkData(this.currentSpineItem.idref, this.getFirstVisibleElementCfi()); } }); define("reflowableView", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * Renders one page of fixed layout spread * @class ReadiumSDK.Views.OnePageView */ //Representation of one fixed page ReadiumSDK.Views.OnePageView = Backbone.View.extend({ currentSpineItem: undefined, spine: undefined, contentAlignment: undefined, //expected 'center' 'left' 'right' meta_size : { width: 0, height: 0 }, initialize: function() { this.spine = this.options.spine; this.contentAlignment = this.options.contentAlignment; }, isDisplaying:function() { return this.currentSpineItem != undefined; }, render: function() { if(!this.$iframe) { this.template = _.template($("#template-ope-fixed-page-view").html(), {}); this.setElement(this.template); this.$el.addClass(this.options.class); this.$iframe = $("iframe", this.$el); } return this; }, remove: function() { this.currentSpineItem = undefined; //base remove Backbone.View.prototype.remove.call(this); }, onIFrameLoad: function(success) { if(success) { var epubContentDocument = this.$iframe[0].contentDocument; this.$epubHtml = $("html", epubContentDocument); this.$epubHtml.css("overflow", "hidden"); this.fitToScreen(); } this.trigger("PageLoaded"); }, fitToScreen: function() { if(!this.isDisplaying()) { return; } this.updateMetaSize(); if(this.meta_size.width <= 0 || this.meta_size.height <= 0) { return; } var containerWidth = this.$el.width(); var containerHeight = this.$el.height(); var horScale = containerWidth / this.meta_size.width; var verScale = containerHeight / this.meta_size.height; var scale = Math.min(horScale, verScale); var newWidth = this.meta_size.width * scale; var newHeight = this.meta_size.height * scale; var top = Math.floor((containerHeight - newHeight) / 2); var left; if(this.contentAlignment == "left") { left = 0; } else if(this.contentAlignment == "right") { left = containerWidth - newWidth; } else { //center left = Math.floor((containerWidth - newWidth) / 2); } if(top < 0) top = 0; if(left < 0) left = 0; var css = this.generateTransformCSS(left, top, scale); css["width"] = this.meta_size.width; css["height"] = this.meta_size.height; this.$epubHtml.css(css); }, generateTransformCSS: function(left, top, scale) { var transformString = "translate(" + left + "px, " + top + "px) scale(" + scale + ")"; //modernizer library can be used to get browser independent transform attributes names (implemented in readium-web fixed_layout_book_zoomer.js) var css = {}; css["-webkit-transform"] = transformString; css["-webkit-transform-origin"] = "0 0"; return css; }, updateMetaSize: function() { var contentDocument = this.$iframe[0].contentDocument; // first try to read viewport size var content = $('meta[name=viewport]', contentDocument).attr("content"); // if not found try viewbox (used for SVG) if(!content) { content = $('meta[name=viewbox]', contentDocument).attr("content"); } if(content) { var size = this.parseSize(content); if(size) { this.meta_size.width = size.width; this.meta_size.height = size.height; } } else { //try to get direct image size var $img = $(contentDocument).find('img'); var width = $img.width(); var height = $img.height(); if( width > 0) { this.meta_size.width = width; this.meta_size.height = height; } } }, loadSpineItem: function(spineItem) { if(this.currentSpineItem != spineItem) { this.currentSpineItem = spineItem; var src = this.spine.getItemUrl(spineItem); ReadiumSDK.Helpers.LoadIframe(this.$iframe[0], src, this.onIFrameLoad, this); } }, parseSize: function(content) { var pairs = content.replace(/\s/g, '').split(","); var dict = {}; for(var i = 0; i < pairs.length; i++) { var nameVal = pairs[i].split("="); if(nameVal.length == 2) { dict[nameVal[0]] = nameVal[1]; } } var width = Number.NaN; var height = Number.NaN; if(dict["width"]) { width = parseInt(dict["width"]); } if(dict["height"]) { height = parseInt(dict["height"]); } if(!isNaN(width) && !isNaN(height)) { return { width: width, height: height} ; } return undefined; }, getFirstVisibleElementCfi: function(){ var navigation = new ReadiumSDK.Views.CfiNavigationLogic(this.$el, this.$iframe); return navigation.getFirstVisibleElementCfi(0); } }); define("onePageView", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /* * View for rendering fixed layout page spread * @class ReadiumSDK.Views.FixedView */ ReadiumSDK.Views.FixedView = Backbone.View.extend({ leftPageView: undefined, rightPageView: undefined, centerPageView: undefined, spine: undefined, spread: undefined, pageViews: [], initialize: function() { this.spine = this.options.spine; this.spread = new ReadiumSDK.Models.Spread(this.spine); this.leftPageView = new ReadiumSDK.Views.OnePageView({spine: this.spine, class: "left_page", contentAlignment: "right"}); this.rightPageView = new ReadiumSDK.Views.OnePageView({spine: this.spine, class: "right_page", contentAlignment: "left"}); this.centerPageView = new ReadiumSDK.Views.OnePageView({spine: this.spine, class: "center_page", contentAlignment: "center"}); this.pageViews.push(this.leftPageView); this.pageViews.push(this.rightPageView); this.pageViews.push(this.centerPageView); //event with namespace for clean unbinding $(window).on("resize.ReadiumSDK.readerView", _.bind(this.onViewportResize, this)); }, isReflowable: function() { return false; }, render: function(){ this.template = _.template($("#template-fixed-view").html(), {}); this.setElement(this.template); this.$spreadWrap = $("#spread-wrap", this.$el); return this; }, remove: function() { $(window).off("resize.ReadiumSDK.readerView"); //base remove Backbone.View.prototype.remove.call(this); }, setViewSettings: function(settings) { this.spread.setSyntheticSpread(settings.isSyntheticSpread); }, redraw: function() { var self = this; var pageLoadDeferrals = this.createPageLoadDeferrals([{pageView: this.leftPageView, spineItem: this.spread.leftItem}, {pageView: this.rightPageView, spineItem: this.spread.rightItem}, {pageView: this.centerPageView, spineItem: this.spread.centerItem}]); if(pageLoadDeferrals.length > 0) { $.when.apply($, pageLoadDeferrals).done(function(){ self.onPagesLoaded() }); } }, createPageLoadDeferrals: function(viewItemPairs) { var pageLoadDeferrals = []; for(var i = 0; i < viewItemPairs.length; i++) { var dfd = this.updatePageViewForItem(viewItemPairs[i].pageView, viewItemPairs[i].spineItem); if(dfd) { pageLoadDeferrals.push(dfd); } } return pageLoadDeferrals; }, onPagesLoaded: function() { this.trigger("ViewPaginationChanged"); }, onViewportResize: function() { for(var i = 0; i < this.pageViews.length; i++) { this.pageViews[i].fitToScreen(); } }, openPage: function(paginationRequest) { if(!paginationRequest.spineItem) { return; } this.spread.openItem(paginationRequest.spineItem); this.redraw(); }, openPagePrev: function() { this.spread.openPrev(); this.redraw(); }, openPageNext: function() { this.spread.openNext(); this.redraw(); }, updatePageViewForItem: function(pageView, item) { if(!item) { if(pageView.isDisplaying()) { pageView.remove(); } return undefined; } if(!pageView.isDisplaying()) { this.$spreadWrap.append(pageView.render().$el); } var dfd = $.Deferred(); pageView.on("PageLoaded", dfd.resolve); pageView.loadSpineItem(item); return dfd.promise(); }, getPaginationInfo: function() { var paginationInfo = new ReadiumSDK.Models.CurrentPagesInfo(this.spine.items.length, this.spine.package.isFixedLayout(), this.spine.direction); var spreadItems = [this.spread.leftItem, this.spread.rightItem, this.spread.centerItem]; for(var i = 0; i < spreadItems.length; i++) { var spreadItem = spreadItems[i]; if(spreadItem) { paginationInfo.addOpenPage(0, 1, spreadItem.idref, spreadItem.index); } } return paginationInfo; }, bookmarkCurrentPage: function() { var viewsToCheck = []; if( this.spine.isLeftToRight() ) { viewsToCheck = [this.leftPageView, this.centerPageView, this.rightPageView]; } else { viewsToCheck = [this.rightPageView, this.centerPageView, this.leftPageView]; } for(var i = 0; i < viewsToCheck.length; i++) { if(viewsToCheck[i].isDisplaying()) { var idref = viewsToCheck[i].currentSpineItem.idref; var cfi = viewsToCheck[i].getFirstVisibleElementCfi(); if(cfi == undefined) { cfi = ""; } return new ReadiumSDK.Models.BookmarkData(idref, cfi); } } return new ReadiumSDK.Models.BookmarkData("", ""); } }); define("fixedView", function(){}); // Created by Boris Schneiderman. // Copyright (c) 2012-2013 The Readium Foundation. // // The Readium SDK is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . /** * * Top level View object. Interface for view manipulation public APIs * * @class ReadiumSDK.Views.ReaderView * * */ ReadiumSDK.Views.ReaderView = Backbone.View.extend({ currentView: undefined, package: undefined, spine: undefined, viewerSettings:undefined, initialize: function() { this.viewerSettings = new ReadiumSDK.Models.ViewerSettings({}); }, renderCurrentView: function(isReflowable) { if(this.currentView){ //current view is already rendered if( this.currentView.isReflowable() === isReflowable) { return; } this.resetCurrentView(); } if(isReflowable) { this.currentView = new ReadiumSDK.Views.ReflowableView({spine:this.spine}); } else { this.currentView = new ReadiumSDK.Views.FixedView({spine:this.spine}); } this.currentView.setViewSettings(this.viewerSettings); this.$el.append(this.currentView.render().$el); var self = this; this.currentView.on("ViewPaginationChanged", function(){ var paginationReportData = self.currentView.getPaginationInfo(); self.trigger("PaginationChanged", paginationReportData); }); }, resetCurrentView: function() { if(!this.currentView) { return; } this.currentView.off("ViewPaginationChanged"); this.currentView.remove(); this.currentView = undefined; }, /** * Triggers the process of opening the book and requesting resources specified in the packageData * * @method openBook * @param {ReadiumSDK.Models.PackageData} packageData DTO Object hierarchy of Package, Spine, SpineItems passed by * host application to the reader * @param {ReadiumSDK.Models.PageOpenRequest|undefined} openPageRequestData Optional parameter specifying * on what page book should be open when it is loaded. If nothing is specified book will be opened on the first page */ openBook: function(packageData, openPageRequestData) { this.package = new ReadiumSDK.Models.Package({packageData: packageData}); this.spine = this.package.spine; this.resetCurrentView(); if(openPageRequestData) { if(openPageRequestData.idref) { if(openPageRequestData.spineItemPageIndex) { this.openSpineItemPage(openPageRequestData.idref, openPageRequestData.spineItemPageIndex); } else if(openPageRequestData.elementCfi) { this.openSpineItemElementCfi(openPageRequestData.idref, openPageRequestData.elementCfi); } else { this.openSpineItemPage(openPageRequestData.idref, 0); } } else if(openPageRequestData.contentRefUrl && openPageRequestData.sourceFileHref) { this.openContentUrl(openPageRequestData.contentRefUrl, openPageRequestData.sourceFileHref); } else { console.log("Invalid page request data: idref required!"); } } else {// if we where not asked to open specific page we will open the first one var spineItem = this.spine.first(); if(spineItem) { var pageOpenRequest = new ReadiumSDK.Models.PageOpenRequest(spineItem); pageOpenRequest.setFirstPage(); this.openPage(pageOpenRequest); } } }, /** * Flips the page from left to right. Takes to account the page progression direction to decide to flip to prev or next page. * @method openPageLeft */ openPageLeft: function() { if(this.package.spine.isLeftToRight()) { this.openPagePrev(); } else { this.openPageNext(); } }, /** * Flips the page from right to left. Takes to account the page progression direction to decide to flip to prev or next page. * @method openPageRight */ openPageRight: function() { if(this.package.spine.isLeftToRight()) { this.openPageNext(); } else { this.openPagePrev(); } }, /** * Updates reader view based on the settings specified in settingsData object * @param settingsData */ updateSettings: function(settingsData) { console.log("UpdateSettings: " + JSON.stringify(settingsData)); this.viewerSettings.update(settingsData); if(this.currentView) { var bookMark = this.currentView.bookmarkCurrentPage(); this.currentView.setViewSettings(this.viewerSettings); if(bookMark) { this.openSpineItemElementCfi(bookMark.idref, bookMark.elementCfi); } } }, /** * Opens the next page. */ openPageNext: function() { var paginationInfo = this.currentView.getPaginationInfo(); if(paginationInfo.openPages.length == 0) { return; } var lastOpenPage = paginationInfo.openPages[paginationInfo.openPages.length - 1]; if(lastOpenPage.spineItemPageIndex < lastOpenPage.spineItemPageCount - 1) { this.currentView.openPageNext(); return; } var currentSpineItem = this.spine.getItemById(lastOpenPage.idref); var nextSpineItem = this.spine.nextItem(currentSpineItem); if(!nextSpineItem) { return; } var openPageRequest = new ReadiumSDK.Models.PageOpenRequest(nextSpineItem); openPageRequest.setFirstPage(); this.openPage(openPageRequest); }, /** * Opens the previews page. */ openPagePrev: function() { var paginationInfo = this.currentView.getPaginationInfo(); if(paginationInfo.openPages.length == 0) { return; } var firstOpenPage = paginationInfo.openPages[0]; if(firstOpenPage.spineItemPageIndex > 0) { this.currentView.openPagePrev(); return; } var currentSpineItem = this.spine.getItemById(firstOpenPage.idref); var prevSpineItem = this.spine.prevItem(currentSpineItem); if(!prevSpineItem) { return; } var openPageRequest = new ReadiumSDK.Models.PageOpenRequest(prevSpineItem); openPageRequest.setLastPage(); this.openPage(openPageRequest); }, getSpineItem: function(idref) { if(!idref) { console.log("idref parameter value missing!"); return undefined; } var spineItem = this.spine.getItemById(idref); if(!spineItem) { console.log("Spine item with id " + idref + " not found!"); return undefined; } return spineItem; }, /** * Opens the page of the spine item with element with provided cfi * * @method openSpineItemElementCfi * * @param {string} idref Id of the spine item * @param {string} elementCfi CFI of the element to be shown */ openSpineItemElementCfi: function(idref, elementCfi) { var spineItem = this.getSpineItem(idref); if(!spineItem) { return; } var pageData = new ReadiumSDK.Models.PageOpenRequest(spineItem); if(elementCfi) { pageData.setElementCfi(elementCfi); } this.openPage(pageData); }, /** * * Opens specified page index of the current spine item * * @method openPageIndex * * @param {number} pageIndex Zero based index of the page in the current spine item */ openPageIndex: function(pageIndex) { if(!this.currentView) { return; } var pageRequest; if(this.package.isFixedLayout()) { var spineItem = this.package.spine.items[pageIndex]; if(!spineItem) { return; } pageRequest = new ReadiumSDK.Models.PageOpenRequest(spineItem); pageRequest.setPageIndex(0); } else { pageRequest = new ReadiumSDK.Models.PageOpenRequest(undefined); pageRequest.setPageIndex(pageIndex); } this.openPage(pageRequest); }, openPage: function(pageRequest) { this.renderCurrentView(pageRequest.spineItem.isReflowable()); this.currentView.openPage(pageRequest); }, /** * * Opens page index of the spine item with idref provided * * @param {string} idref Id of the spine item * @param {number} pageIndex Zero based index of the page in the spine item */ openSpineItemPage: function(idref, pageIndex) { var spineItem = this.getSpineItem(idref); if(!spineItem) { return; } var pageData = new ReadiumSDK.Models.PageOpenRequest(spineItem); if(pageIndex) { pageData.setPageIndex(pageIndex); } this.openPage(pageData); }, /** * Opens the content document specified by the url * * @method openContentUrl * * @param {string} contentRefUrl Url of the content document * @param {string | undefined} sourceFileHref Url to the file that contentRefUrl is relative to. If contentRefUrl is * relative ot the source file that contains it instead of the package file (ex. TOC file) We have to know the * sourceFileHref to resolve contentUrl relative to the package file. * */ openContentUrl: function(contentRefUrl, sourceFileHref) { var combinedPath = ReadiumSDK.Helpers.ResolveContentRef(contentRefUrl, sourceFileHref); var hashIndex = combinedPath.indexOf("#"); var hrefPart; var elementId; if(hashIndex >= 0) { hrefPart = combinedPath.substr(0, hashIndex); elementId = combinedPath.substr(hashIndex + 1); } else { hrefPart = combinedPath; elementId = undefined; } var spineItem = this.spine.getItemByHref(hrefPart); if(!spineItem) { return; } var pageData = new ReadiumSDK.Models.PageOpenRequest(spineItem) if(elementId){ pageData.setElementId(elementId); } this.openPage(pageData); }, /** * * Returns the bookmark associated with currently opened page. * * @method bookmarkCurrentPage * * @returns {string} Stringified ReadiumSDK.Models.BookmarkData object. */ bookmarkCurrentPage: function() { return JSON.stringify(this.currentView.bookmarkCurrentPage()); } }); define("readerView", function(){}); define('epub_renderer_module',['require', 'module', 'jquery', 'underscore', 'backbone', 'readiumSDK', 'helpers', 'triggers', 'bookmarkData', 'spineItem', 'spine', 'fixedPageSpread', 'package', 'viewerSettings', 'currentPagesInfo', 'pageOpenRequest', 'epubCfi', 'cfiNavigationLogic', 'reflowableView', 'onePageView', 'fixedView', 'readerView'], function (require, module, $, _, Backbone, ReadiumSDK, helpers, triggers, bookmarkData, spineItem, spine, fixedPageSpread, package, viewerSettings, currentPagesInfo, pageOpenRequest, epubCfi, cfiNavigationLogic, reflowableView, onePageView, fixedView, readerView ) { var ReadiumSDK; } ); var origLoadIframeFunction = ReadiumSDK.Helpers.LoadIframe; var origReadiumSDKModelSpineItem = ReadiumSDK.Models.SpineItem; ReadiumSDK.Models.SpineItem = function(itemData, index, spine) { function SpineItem() { this.media_type = itemData.media_type; } SpineItem.prototype = new origReadiumSDKModelSpineItem(itemData, index, spine); SpineItem.constructor = SpineItem; return new SpineItem(); }; var loadIframeFunctionGenerator = function(epubFetch, reader) { return function(iframe, src, origCallback, context) { var callback = function(success) { var epubContentDocument = this.$iframe[0].contentDocument; $('a', epubContentDocument).click(function (clickEvent) { // Check for both href and xlink:href attribute and get value var href; if (clickEvent.currentTarget.attributes["xlink:href"]) { href = clickEvent.currentTarget.attributes["xlink:href"].value; } else { href = clickEvent.currentTarget.attributes["href"].value; } var hrefUri = new URI(href); var hrefIsRelative = hrefUri.is('relative'); var hrefUriHasFilename = hrefUri.filename(); var overrideClickEvent = false; if (hrefIsRelative) { // TODO: if (hrefUriHasFilename /* TODO: && check whether href actually resolves to a spine item */) { var currentSpineItemUri = new URI(context.currentSpineItem.href); var openedSpineItemUri = hrefUri.absoluteTo(currentSpineItemUri); var idref = openedSpineItemUri.pathname(); var hashFrag = openedSpineItemUri.fragment(); var spineItem = context.spine.getItemByHref(idref); var pageData = new ReadiumSDK.Models.PageOpenRequest(spineItem); if (hashFrag) { pageData.setElementId(hashFrag); } reader.openPage(pageData); overrideClickEvent = true; } // otherwise it's probably just a hash frag that needs to be handled by browser's default handling } else { // It's an absolute URL to a remote site - open it in a separate window outside the reader window.open(href, '_blank'); overrideClickEvent = true; } if (overrideClickEvent) { clickEvent.preventDefault(); clickEvent.stopPropagation(); } }); origCallback.call(this, success); } if (epubFetch.isPackageExploded()) { return origLoadIframeFunction(iframe, src, callback, context); } else { var onLoadWrapperFunction = function(success) { var context = this; var itemHref = context.currentSpineItem.href; epubFetch.relativeToPackageFetchFileContents(itemHref, 'text', function(contentDocumentText) { var srcMediaType = context.currentSpineItem.media_type; epubFetch.resolveInternalPackageResources(itemHref, srcMediaType, contentDocumentText, function (resolvedContentDocumentDom) { var contentDocument = iframe.contentDocument; contentDocument.replaceChild(resolvedContentDocumentDom.documentElement, contentDocument.documentElement); callback.call(context, success); }); }, function(err) { if (err.message) { console.error(err.message); }; console.error(err); callback.call(context, success); }); }; // Feed an artificial empty HTML document to the IFRAME, then let the wrapper onload function // take care of actual document loading (from zipped EPUB) and calling callbacks: var emptyDocumentDataUri = window.URL.createObjectURL( new Blob([''], {'type': 'text/html'}) ); return origLoadIframeFunction(iframe, emptyDocumentDataUri, onLoadWrapperFunction, context); } }; }; var EpubRendererModule = function (epubFetch, elementToBindReaderTo, packageData) { var reader = new ReadiumSDK.Views.ReaderView({ el: elementToBindReaderTo }); /* * Patch the ReadiumSDK.Helpers.LoadIframe global function to support zipped EPUB packages: */ ReadiumSDK.Helpers.LoadIframe = loadIframeFunctionGenerator(epubFetch, reader); // Description: The public interface return { openBook : function () { return reader.openBook(packageData); }, openSpineItemElementCfi : function (idref, elementCfi) { return reader.openSpineItemElementCfi(idref, elementCfi); }, openSpineItemPage: function(idref, pageIndex) { return reader.openSpineItemPage(idref, pageIndex); }, openPageIndex: function(pageIndex) { return reader.openPageIndex(pageIndex); }, openPageRight : function () { return reader.openPageRight(); }, openPageLeft : function () { return reader.openPageLeft(); }, updateSettings : function (settingsData) { return reader.updateSettings(settingsData); }, bookmarkCurrentPage : function () { return reader.bookmarkCurrentPage(); } }; }; return EpubRendererModule; }); define('Readium',['require', 'module', 'jquery', 'underscore', 'backbone', 'epub_fetch_module', 'epub_module', 'epub_reading_system', 'epub_renderer_module'], function (require, module, $, _, Backbone, EpubFetchModule, EpubModule, EpubReadingSystem, EpubRendererModule) { /** * Creates an instance of the Readium.js object. * * @constructor * @param elementToBindReaderTo The document element to bind display of the reader to. * @param packageDocumentURL : The URL to the package document * @param jsLibDir The path (relative to the current document) in which dependant zip.js libraries can be found. * @param definitionCallback The callback function that asynchronously receives the object's public interface once it has been initialized (document has been parsed). */ var Readium = function (elementToBindReaderTo, packageDocumentURL, jsLibDir, definitionCallback) { // -------------- Initialization of viewer ------------------ // var epubFetch = new EpubFetchModule({ packageDocumentURL: packageDocumentURL, libDir: jsLibDir }); var epub = new EpubModule(epubFetch, function () { var renderer = new EpubRendererModule(epubFetch, elementToBindReaderTo, epub.getPackageData()); // Readium.js module api definitionCallback({ openBook : function () { return renderer.openBook(); }, openSpineItemElementCfi : function (idref, elementCfi) { return renderer.openSpineItemElementCfi(idref, elementCfi); }, openSpineItemPage: function(idref, pageIndex) { return renderer.openSpineItemPage(idref, pageIndex); }, openPageIndex: function(pageIndex) { return renderer.openPageIndex(pageIndex); }, openPageRight : function () { return renderer.openPageRight(); }, openPageLeft : function () { return renderer.openPageLeft(); }, updateSettings : function (settingsData) { return renderer.updateSettings(settingsData); }, bookmarkCurrentPage : function () { return renderer.bookmarkCurrentPage(); } }); }); }; // Note: the epubReadingSystem object may not be ready when directly using the // window.onload callback function (from within an (X)HTML5 EPUB3 content document's Javascript code) // To address this issue, the recommended code is: // ----- // console.log(navigator.epubReadingSystem); return Readium; });