1
0
Fork 0
mirror of https://github.com/futurepress/epub.js.git synced 2025-10-02 14:49:16 +02:00

Restructure Epub and Book into per format classes

This commit is contained in:
Fred Chasen 2022-02-07 14:38:28 -08:00
parent f6fece4f88
commit e714ac778e
158 changed files with 6212 additions and 45349 deletions

View file

@ -190,8 +190,8 @@ rendition.hooks.content.register(function(contents, view) {
The parts of the rendering process that can be hooked into are below.
```js
book.spine.hooks.serialize // Section is being converted to text
book.spine.hooks.content // Section has been loaded and parsed
rendition.hooks.serialize // Section is being converted to text
rendition.hooks.content // Section has been loaded and parsed
rendition.hooks.render // Section is rendered to the screen
rendition.hooks.content // Section contents have been loaded
rendition.hooks.unloaded // Section contents are being unloaded

View file

@ -1,39 +0,0 @@
{
"name": "epubjs",
"version": "0.3.0",
"authors": [
"Fred Chasen <fchasen@gmail.com>"
],
"description": "Enhanced eBooks in the browser.",
"main": "dist/epub.js",
"moduleType": [
"amd",
"globals",
"node"
],
"keywords": [
"epub"
],
"license": "MIT",
"homepage": "http://futurepress.org",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tools",
"books",
"examples"
],
"dependencies": {
"event-emitter": "^0.3.5",
"jszip": "^3.4.0",
"localforage": "^1.7.3",
"lodash": "^4.17.15",
"marks-pane": "^1.0.9",
"path-webpack": "0.0.3",
"stream-browserify": "^3.0.0",
"url-polyfill": "^1.1.9",
"xmldom": "^0.3.0"
}
}

File diff suppressed because it is too large Load diff

164
examples/test-manifest.html Normal file
View file

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EPUB.js Spreads Example</title>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script> -->
<!-- <script src="../dist/epub.js"></script> -->
<!-- <script src="../src/index.js" type="module"></script> -->
<!-- <script type="importmap">
{
"imports": {
"event-emitter" : "../node_modules/event-emitter/index.js",
}
}
</script> -->
<link rel="stylesheet" type="text/css" href="examples.css">
</head>
<body>
<!-- <div id="title"></div> -->
<select id="toc"></select>
<div id="viewer" class="spreads"></div>
<a id="prev" href="#prev" class="arrow"></a>
<a id="next" href="#next" class="arrow"></a>
<script type="module">
import { Manifest, Rendition } from "../src/index.js"
const params = URLSearchParams && new URLSearchParams(document.location.search.substring(1));
const url = params && params.get("url") && decodeURIComponent(params.get("url"));
const currentSectionIndex = (params && params.get("loc")) ? params.get("loc") : undefined;
// Load the opf
// let book = await new Epub(url || "https://s3.amazonaws.com/epubjs/books/moby-dick/OPS/package.opf");
let book = await new Manifest(url || "/books/alice-manifest/manifest.jsonld");
book.opened.then(() => {
console.log("Manifest", book);
})
let rendition = new Rendition(book, {
element: "viewer",
width: "100%",
height: 600,
spread: "always"
});
let displayed = await rendition.display(currentSectionIndex);
let next = document.getElementById("next");
next.addEventListener("click", function (e) {
book.metadata.readingProgression === "rtl" ? rendition.prev() : rendition.next();
e.preventDefault();
}, false);
let prev = document.getElementById("prev");
prev.addEventListener("click", function (e) {
book.metadata.readingProgression === "rtl" ? rendition.next() : rendition.prev();
e.preventDefault();
}, false);
let keyListener = function (e) {
// Left Key
if ((e.keyCode || e.which) == 37) {
book.metadata.readingProgression === "rtl" ? rendition.next() : rendition.prev();
}
// Right Key
if ((e.keyCode || e.which) == 39) {
book.metadata.readingProgression === "rtl" ? rendition.prev() : rendition.next();
}
};
rendition.on("keyup", keyListener);
document.addEventListener("keyup", keyListener, false);
var title = document.getElementById("title");
rendition.on("rendered", function(section){
var current = book.navigation && book.navigation.get(section.url);
if (current) {
var $select = document.getElementById("toc");
var $selected = $select.querySelector("option[selected]");
if ($selected) {
$selected.removeAttribute("selected");
}
var $options = $select.querySelectorAll("option");
for (var i = 0; i < $options.length; ++i) {
let selected = $options[i].getAttribute("ref") === current.url;
if (selected) {
$options[i].setAttribute("selected", "");
}
}
}
});
rendition.on("relocated", function(location){
console.log(location);
var next = book.metadata.readingProgression === "rtl" ? document.getElementById("prev") : document.getElementById("next");
var prev = book.metadata.readingProgression === "rtl" ? document.getElementById("next") : document.getElementById("prev");
if (location.atEnd) {
next.style.visibility = "hidden";
} else {
next.style.visibility = "visible";
}
if (location.atStart) {
prev.style.visibility = "hidden";
} else {
prev.style.visibility = "visible";
}
});
rendition.on("layout", function(layout) {
let viewer = document.getElementById("viewer");
if (layout.spread) {
viewer.classList.remove('single');
} else {
viewer.classList.add('single');
}
});
window.addEventListener("unload", function () {
rendition.destroy();
book.destroy();
});
let $select = document.getElementById("toc");
let docfrag = document.createDocumentFragment();
book.navigation.forEach(function(chapter) {
let option = document.createElement("option");
option.textContent = chapter.name;
option.setAttribute("ref", chapter.url);
docfrag.appendChild(option);
});
$select.appendChild(docfrag);
$select.onchange = function(){
let index = $select.selectedIndex;
let url = $select.options[index].getAttribute("ref");
rendition.display(url);
return false;
};
</script>
</body>
</html>

165
examples/test.html Normal file
View file

@ -0,0 +1,165 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EPUB.js Spreads Example</title>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script> -->
<!-- <script src="../dist/epub.js"></script> -->
<!-- <script src="../src/index.js" type="module"></script> -->
<!-- <script type="importmap">
{
"imports": {
"event-emitter" : "../node_modules/event-emitter/index.js",
}
}
</script> -->
<link rel="stylesheet" type="text/css" href="examples.css">
</head>
<body>
<!-- <div id="title"></div> -->
<select id="toc"></select>
<div id="viewer" class="spreads"></div>
<a id="prev" href="#prev" class="arrow"></a>
<a id="next" href="#next" class="arrow"></a>
<script type="module">
import { Epub, Rendition, EpubCFI } from "../src/index.js"
const params = URLSearchParams && new URLSearchParams(document.location.search.substring(1));
const url = params && params.get("url") && decodeURIComponent(params.get("url"));
const currentSectionIndex = (params && params.get("loc")) ? params.get("loc") : undefined;
// Load the opf
let book = await new Epub(url || "https://s3.amazonaws.com/epubjs/books/moby-dick/OPS/package.opf");
book.opened.then(() => {
console.log("Epub", book);
})
let rendition = new Rendition(book, {
element: "viewer",
width: "100%",
height: 600,
spread: "always",
manager: "continuous",
flow: "paginated"
});
let displayed = await rendition.display(currentSectionIndex);
let next = document.getElementById("next");
next.addEventListener("click", function (e) {
book.metadata.direction === "rtl" ? rendition.prev() : rendition.next();
e.preventDefault();
}, false);
let prev = document.getElementById("prev");
prev.addEventListener("click", function (e) {
book.metadata.direction === "rtl" ? rendition.next() : rendition.prev();
e.preventDefault();
}, false);
let keyListener = function (e) {
// Left Key
if ((e.keyCode || e.which) == 37) {
book.metadata.direction === "rtl" ? rendition.next() : rendition.prev();
}
// Right Key
if ((e.keyCode || e.which) == 39) {
book.metadata.direction === "rtl" ? rendition.prev() : rendition.next();
}
};
rendition.on("keyup", keyListener);
document.addEventListener("keyup", keyListener, false);
var title = document.getElementById("title");
rendition.on("rendered", function(section){
var current = book.navigation && book.navigation.get(section.url);
if (current) {
var $select = document.getElementById("toc");
var $selected = $select.querySelector("option[selected]");
if ($selected) {
$selected.removeAttribute("selected");
}
var $options = $select.querySelectorAll("option");
for (var i = 0; i < $options.length; ++i) {
let selected = $options[i].getAttribute("ref") === current.url;
if (selected) {
$options[i].setAttribute("selected", "");
}
}
}
});
rendition.on("relocated", function(location){
console.log(location);
var next = book.metadata.direction === "rtl" ? document.getElementById("prev") : document.getElementById("next");
var prev = book.metadata.direction === "rtl" ? document.getElementById("next") : document.getElementById("prev");
if (location.atEnd) {
next.style.visibility = "hidden";
} else {
next.style.visibility = "visible";
}
if (location.atStart) {
prev.style.visibility = "hidden";
} else {
prev.style.visibility = "visible";
}
});
rendition.on("layout", function(layout) {
let viewer = document.getElementById("viewer");
if (layout.spread) {
viewer.classList.remove('single');
} else {
viewer.classList.add('single');
}
});
window.addEventListener("unload", function () {
rendition.destroy();
book.destroy();
});
let $select = document.getElementById("toc");
let docfrag = document.createDocumentFragment();
book.navigation.forEach(function(chapter) {
let option = document.createElement("option");
option.textContent = chapter.name;
option.setAttribute("ref", chapter.url);
docfrag.appendChild(option);
});
$select.appendChild(docfrag);
$select.onchange = function(){
let index = $select.selectedIndex;
let url = $select.options[index].getAttribute("ref");
rendition.display(url);
return false;
};
</script>
</body>
</html>

36844
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,68 +1,36 @@
{
"name": "epubjs",
"version": "0.3.92",
"version": "0.5",
"description": "Parse and Render Epubs",
"main": "lib/index.js",
"module": "src/index.js",
"browser": "dist/epub.js",
"types": "types/index.d.ts",
"repository": "https://github.com/futurepress/epub.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "karma start --single-run --browsers ChromeHeadlessNoSandbox",
"docs": "documentation build src/epub.js -f html -o documentation/html/",
"docs:html": "documentation build src/epub.js -f html -o documentation/html/",
"docs:md": "documentation build src/epub.js -f md -o documentation/md/API.md",
"test": "",
"lint": "eslint -c .eslintrc.js src; exit 0",
"start": "webpack-dev-server --inline --d",
"build": "NODE_ENV=production webpack --progress",
"minify": "NODE_ENV=production MINIMIZE=true webpack --progress",
"legacy": "NODE_ENV=production LEGACY=true webpack --progress",
"productionLegacy": "NODE_ENV=production MINIMIZE=true LEGACY=true webpack --progress",
"compile": "babel -d lib/ src/",
"watch": "babel --watch -d lib/ src/",
"prepare": "npm run compile && npm run build && npm run minify && npm run legacy && npm run productionLegacy"
"start": "rollup -w -c rollup.server.config.js",
"build": "rollup -c",
"compile": "babel src/ -d lib/",
"watch": "babel --watch --optional runtime -d lib/ src/",
"prepare": "npm run compile && npm run build"
},
"author": "fchasen@gmail.com",
"license": "BSD-2-Clause",
"devDependencies": {
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-export-default-from": "^7.14.5",
"@babel/plugin-proposal-export-namespace-from": "^7.14.5",
"@babel/preset-env": "^7.15.8",
"@babel/runtime": "^7.15.4",
"babel-loader": "^8.2.3",
"documentation": "^13.2.5",
"eslint": "^8.0.1",
"jsdoc": "^3.6.7",
"karma": "^5.0.9",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^4.0.2",
"mocha": "^7.2.0",
"mocha-loader": "^5.0.0",
"raw-loader": "^4.0.2",
"terser-webpack-plugin": "^3.0.3",
"tsd-jsdoc": "^2.5.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-middleware": "^3.7.2",
"webpack-dev-server": "^3.11.0"
"@babel/core": "^7.16.7",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.3",
"rollup": "^2.63.0",
"rollup-plugin-license": "^2.6.1",
"rollup-plugin-serve": "^1.1.0",
"serve": "^13.0.2"
},
"dependencies": {
"@types/localforage": "0.0.34",
"@xmldom/xmldom": "^0.7.5",
"core-js": "^3.18.3",
"event-emitter": "^0.3.5",
"@xmldom/xmldom": "^0.8.0",
"jszip": "^3.7.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"marks-pane": "^1.0.9",
"path-webpack": "0.0.3"
"lodash": "^4.17.21"
}
}

35
rollup.config.js Normal file
View file

@ -0,0 +1,35 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import license from "rollup-plugin-license";
import pkg from "./package.json";
const plugins = [
nodeResolve(),
commonjs(),
license({
banner: "@license Epub.js v<%= pkg.version %> | BSD-2-Clause | https://github.com/futurepress/epub.js",
})
];
export default [
{
input: pkg.module,
output: {
name: "ePub",
file: pkg.browser,
format: "umd"
},
plugins: plugins
},
{
input: pkg.module,
output: {
name: "ePub",
file: "./dist/epub.esm.js",
format: "es"
},
plugins: plugins
}
];

42
rollup.server.config.js Normal file
View file

@ -0,0 +1,42 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import pkg from "./package.json";
import serve from 'rollup-plugin-serve'
import livereload from 'rollup-plugin-livereload'
const plugins = [
nodeResolve(),
commonjs()
];
export default [
{
input: pkg.module,
output: {
name: 'ePub',
file: pkg.browser,
format: 'umd',
globals: {
jszip: 'JSZip',
xmldom: 'xmldom'
},
sourcemap: 'inline'
},
plugins: plugins.concat([
serve({
port: 8080,
contentBase: './',
headers: {
"Access-Control-Allow-Origin": "*",
"Service-Worker-Allowed": "/",
}
}),
livereload({
watch: ['dist', 'examples']
})
]),
external: ['jszip', 'xmldom'],
}
];

View file

@ -1,768 +0,0 @@
import EventEmitter from "event-emitter";
import {extend, defer} from "./utils/core";
import Url from "./utils/url";
import Path from "./utils/path";
import Spine from "./spine";
import Locations from "./locations";
import Container from "./container";
import Packaging from "./packaging";
import Navigation from "./navigation";
import Resources from "./resources";
import PageList from "./pagelist";
import Rendition from "./rendition";
import Archive from "./archive";
import request from "./utils/request";
import EpubCFI from "./epubcfi";
import Store from "./store";
import DisplayOptions from "./displayoptions";
import { EPUBJS_VERSION, EVENTS } from "./utils/constants";
const CONTAINER_PATH = "META-INF/container.xml";
const IBOOKS_DISPLAY_OPTIONS_PATH = "META-INF/com.apple.ibooks.display-options.xml";
const INPUT_TYPE = {
BINARY: "binary",
BASE64: "base64",
EPUB: "epub",
OPF: "opf",
MANIFEST: "json",
DIRECTORY: "directory"
};
/**
* An Epub representation with methods for the loading, parsing and manipulation
* of its contents.
* @class
* @param {string} [url]
* @param {object} [options]
* @param {method} [options.requestMethod] a request function to use instead of the default
* @param {boolean} [options.requestCredentials=undefined] send the xhr request withCredentials
* @param {object} [options.requestHeaders=undefined] send the xhr request headers
* @param {string} [options.encoding=binary] optional to pass 'binary' or base64' for archived Epubs
* @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs
* @param {method} [options.canonical] optional function to determine canonical urls for a path
* @param {string} [options.openAs] optional string to determine the input type
* @param {string} [options.store=false] cache the contents in local storage, value should be the name of the reader
* @returns {Book}
* @example new Book("/path/to/book.epub", {})
* @example new Book({ replacements: "blobUrl" })
*/
class Book {
constructor(url, options) {
// Allow passing just options to the Book
if (typeof(options) === "undefined" &&
typeof(url) !== "string" &&
url instanceof Blob === false &&
url instanceof ArrayBuffer === false) {
options = url;
url = undefined;
}
this.settings = extend(this.settings || {}, {
requestMethod: undefined,
requestCredentials: undefined,
requestHeaders: undefined,
encoding: undefined,
replacements: undefined,
canonical: undefined,
openAs: undefined,
store: undefined
});
extend(this.settings, options);
// Promises
this.opening = new defer();
/**
* @member {promise} opened returns after the book is loaded
* @memberof Book
*/
this.opened = this.opening.promise;
this.isOpen = false;
this.loading = {
manifest: new defer(),
spine: new defer(),
metadata: new defer(),
cover: new defer(),
navigation: new defer(),
pageList: new defer(),
resources: new defer(),
displayOptions: new defer()
};
this.loaded = {
manifest: this.loading.manifest.promise,
spine: this.loading.spine.promise,
metadata: this.loading.metadata.promise,
cover: this.loading.cover.promise,
navigation: this.loading.navigation.promise,
pageList: this.loading.pageList.promise,
resources: this.loading.resources.promise,
displayOptions: this.loading.displayOptions.promise
};
/**
* @member {promise} ready returns after the book is loaded and parsed
* @memberof Book
* @private
*/
this.ready = Promise.all([
this.loaded.manifest,
this.loaded.spine,
this.loaded.metadata,
this.loaded.cover,
this.loaded.navigation,
this.loaded.resources,
this.loaded.displayOptions
]);
// Queue for methods used before opening
this.isRendered = false;
// this._q = queue(this);
/**
* @member {method} request
* @memberof Book
* @private
*/
this.request = this.settings.requestMethod || request;
/**
* @member {Spine} spine
* @memberof Book
*/
this.spine = new Spine();
/**
* @member {Locations} locations
* @memberof Book
*/
this.locations = new Locations(this.spine, this.load.bind(this));
/**
* @member {Navigation} navigation
* @memberof Book
*/
this.navigation = undefined;
/**
* @member {PageList} pagelist
* @memberof Book
*/
this.pageList = undefined;
/**
* @member {Url} url
* @memberof Book
* @private
*/
this.url = undefined;
/**
* @member {Path} path
* @memberof Book
* @private
*/
this.path = undefined;
/**
* @member {boolean} archived
* @memberof Book
* @private
*/
this.archived = false;
/**
* @member {Archive} archive
* @memberof Book
* @private
*/
this.archive = undefined;
/**
* @member {Store} storage
* @memberof Book
* @private
*/
this.storage = undefined;
/**
* @member {Resources} resources
* @memberof Book
* @private
*/
this.resources = undefined;
/**
* @member {Rendition} rendition
* @memberof Book
* @private
*/
this.rendition = undefined;
/**
* @member {Container} container
* @memberof Book
* @private
*/
this.container = undefined;
/**
* @member {Packaging} packaging
* @memberof Book
* @private
*/
this.packaging = undefined;
/**
* @member {DisplayOptions} displayOptions
* @memberof DisplayOptions
* @private
*/
this.displayOptions = undefined;
// this.toc = undefined;
if (this.settings.store) {
this.store(this.settings.store);
}
if(url) {
this.open(url, this.settings.openAs).catch((error) => {
var err = new Error("Cannot load book at "+ url );
this.emit(EVENTS.BOOK.OPEN_FAILED, err);
});
}
}
/**
* Open a epub or url
* @param {string | ArrayBuffer} input Url, Path or ArrayBuffer
* @param {string} [what="binary", "base64", "epub", "opf", "json", "directory"] force opening as a certain type
* @returns {Promise} of when the book has been loaded
* @example book.open("/path/to/book.epub")
*/
open(input, what) {
var opening;
var type = what || this.determineType(input);
if (type === INPUT_TYPE.BINARY) {
this.archived = true;
this.url = new Url("/", "");
opening = this.openEpub(input);
} else if (type === INPUT_TYPE.BASE64) {
this.archived = true;
this.url = new Url("/", "");
opening = this.openEpub(input, type);
} else if (type === INPUT_TYPE.EPUB) {
this.archived = true;
this.url = new Url("/", "");
opening = this.request(input, "binary", this.settings.requestCredentials, this.settings.requestHeaders)
.then(this.openEpub.bind(this));
} else if(type == INPUT_TYPE.OPF) {
this.url = new Url(input);
opening = this.openPackaging(this.url.Path.toString());
} else if(type == INPUT_TYPE.MANIFEST) {
this.url = new Url(input);
opening = this.openManifest(this.url.Path.toString());
} else {
this.url = new Url(input);
opening = this.openContainer(CONTAINER_PATH)
.then(this.openPackaging.bind(this));
}
return opening;
}
/**
* Open an archived epub
* @private
* @param {binary} data
* @param {string} [encoding]
* @return {Promise}
*/
openEpub(data, encoding) {
return this.unarchive(data, encoding || this.settings.encoding)
.then(() => {
return this.openContainer(CONTAINER_PATH);
})
.then((packagePath) => {
return this.openPackaging(packagePath);
});
}
/**
* Open the epub container
* @private
* @param {string} url
* @return {string} packagePath
*/
openContainer(url) {
return this.load(url)
.then((xml) => {
this.container = new Container(xml);
return this.resolve(this.container.packagePath);
});
}
/**
* Open the Open Packaging Format Xml
* @private
* @param {string} url
* @return {Promise}
*/
openPackaging(url) {
this.path = new Path(url);
return this.load(url)
.then((xml) => {
this.packaging = new Packaging(xml);
return this.unpack(this.packaging);
});
}
/**
* Open the manifest JSON
* @private
* @param {string} url
* @return {Promise}
*/
openManifest(url) {
this.path = new Path(url);
return this.load(url)
.then((json) => {
this.packaging = new Packaging();
this.packaging.load(json);
return this.unpack(this.packaging);
});
}
/**
* Load a resource from the Book
* @param {string} path path to the resource to load
* @return {Promise} returns a promise with the requested resource
*/
load(path) {
var resolved = this.resolve(path);
if(this.archived) {
return this.archive.request(resolved);
} else {
return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders);
}
}
/**
* Resolve a path to it's absolute position in the Book
* @param {string} path
* @param {boolean} [absolute] force resolving the full URL
* @return {string} the resolved path string
*/
resolve(path, absolute) {
if (!path) {
return;
}
var resolved = path;
var isAbsolute = (path.indexOf("://") > -1);
if (isAbsolute) {
return path;
}
if (this.path) {
resolved = this.path.resolve(path);
}
if(absolute != false && this.url) {
resolved = this.url.resolve(resolved);
}
return resolved;
}
/**
* Get a canonical link to a path
* @param {string} path
* @return {string} the canonical path string
*/
canonical(path) {
var url = path;
if (!path) {
return "";
}
if (this.settings.canonical) {
url = this.settings.canonical(path);
} else {
url = this.resolve(path, true);
}
return url;
}
/**
* Determine the type of they input passed to open
* @private
* @param {string} input
* @return {string} binary | directory | epub | opf
*/
determineType(input) {
var url;
var path;
var extension;
if (this.settings.encoding === "base64") {
return INPUT_TYPE.BASE64;
}
if(typeof(input) != "string") {
return INPUT_TYPE.BINARY;
}
url = new Url(input);
path = url.path();
extension = path.extension;
// If there's a search string, remove it before determining type
if (extension) {
extension = extension.replace(/\?.*$/, "");
}
if (!extension) {
return INPUT_TYPE.DIRECTORY;
}
if(extension === "epub"){
return INPUT_TYPE.EPUB;
}
if(extension === "opf"){
return INPUT_TYPE.OPF;
}
if(extension === "json"){
return INPUT_TYPE.MANIFEST;
}
}
/**
* unpack the contents of the Books packaging
* @private
* @param {Packaging} packaging object
*/
unpack(packaging) {
this.package = packaging; //TODO: deprecated this
if (this.packaging.metadata.layout === "") {
// rendition:layout not set - check display options if book is pre-paginated
this.load(this.url.resolve(IBOOKS_DISPLAY_OPTIONS_PATH)).then((xml) => {
this.displayOptions = new DisplayOptions(xml);
this.loading.displayOptions.resolve(this.displayOptions);
}).catch((err) => {
this.displayOptions = new DisplayOptions();
this.loading.displayOptions.resolve(this.displayOptions);
});
} else {
this.displayOptions = new DisplayOptions();
this.loading.displayOptions.resolve(this.displayOptions);
}
this.spine.unpack(this.packaging, this.resolve.bind(this), this.canonical.bind(this));
this.resources = new Resources(this.packaging.manifest, {
archive: this.archive,
resolver: this.resolve.bind(this),
request: this.request.bind(this),
replacements: this.settings.replacements || (this.archived ? "blobUrl" : "base64")
});
this.loadNavigation(this.packaging).then(() => {
// this.toc = this.navigation.toc;
this.loading.navigation.resolve(this.navigation);
});
if (this.packaging.coverPath) {
this.cover = this.resolve(this.packaging.coverPath);
}
// Resolve promises
this.loading.manifest.resolve(this.packaging.manifest);
this.loading.metadata.resolve(this.packaging.metadata);
this.loading.spine.resolve(this.spine);
this.loading.cover.resolve(this.cover);
this.loading.resources.resolve(this.resources);
this.loading.pageList.resolve(this.pageList);
this.isOpen = true;
if(this.archived || this.settings.replacements && this.settings.replacements != "none") {
this.replacements().then(() => {
this.loaded.displayOptions.then(() => {
this.opening.resolve(this);
});
})
.catch((err) => {
console.error(err);
});
} else {
// Resolve book opened promise
this.loaded.displayOptions.then(() => {
this.opening.resolve(this);
});
}
}
/**
* Load Navigation and PageList from package
* @private
* @param {Packaging} packaging
*/
loadNavigation(packaging) {
let navPath = packaging.navPath || packaging.ncxPath;
let toc = packaging.toc;
// From json manifest
if (toc) {
return new Promise((resolve, reject) => {
this.navigation = new Navigation(toc);
if (packaging.pageList) {
this.pageList = new PageList(packaging.pageList); // TODO: handle page lists from Manifest
}
resolve(this.navigation);
});
}
if (!navPath) {
return new Promise((resolve, reject) => {
this.navigation = new Navigation();
this.pageList = new PageList();
resolve(this.navigation);
});
}
return this.load(navPath, "xml")
.then((xml) => {
this.navigation = new Navigation(xml);
this.pageList = new PageList(xml);
return this.navigation;
});
}
/**
* Gets a Section of the Book from the Spine
* Alias for `book.spine.get`
* @param {string} target
* @return {Section}
*/
section(target) {
return this.spine.get(target);
}
/**
* Sugar to render a book to an element
* @param {element | string} element element or string to add a rendition to
* @param {object} [options]
* @return {Rendition}
*/
renderTo(element, options) {
this.rendition = new Rendition(this, options);
this.rendition.attachTo(element);
return this.rendition;
}
/**
* Set if request should use withCredentials
* @param {boolean} credentials
*/
setRequestCredentials(credentials) {
this.settings.requestCredentials = credentials;
}
/**
* Set headers request should use
* @param {object} headers
*/
setRequestHeaders(headers) {
this.settings.requestHeaders = headers;
}
/**
* Unarchive a zipped epub
* @private
* @param {binary} input epub data
* @param {string} [encoding]
* @return {Archive}
*/
unarchive(input, encoding) {
this.archive = new Archive();
return this.archive.open(input, encoding);
}
/**
* Store the epubs contents
* @private
* @param {binary} input epub data
* @param {string} [encoding]
* @return {Store}
*/
store(name) {
// Use "blobUrl" or "base64" for replacements
let replacementsSetting = this.settings.replacements && this.settings.replacements !== "none";
// Save original url
let originalUrl = this.url;
// Save original request method
let requester = this.settings.requestMethod || request.bind(this);
// Create new Store
this.storage = new Store(name, requester, this.resolve.bind(this));
// Replace request method to go through store
this.request = this.storage.request.bind(this.storage);
this.opened.then(() => {
if (this.archived) {
this.storage.requester = this.archive.request.bind(this.archive);
}
// Substitute hook
let substituteResources = (output, section) => {
section.output = this.resources.substitute(output, section.url);
};
// Set to use replacements
this.resources.settings.replacements = replacementsSetting || "blobUrl";
// Create replacement urls
this.resources.replacements().
then(() => {
return this.resources.replaceCss();
});
this.storage.on("offline", () => {
// Remove url to use relative resolving for hrefs
this.url = new Url("/", "");
// Add hook to replace resources in contents
this.spine.hooks.serialize.register(substituteResources);
});
this.storage.on("online", () => {
// Restore original url
this.url = originalUrl;
// Remove hook
this.spine.hooks.serialize.deregister(substituteResources);
});
});
return this.storage;
}
/**
* Get the cover url
* @return {Promise<?string>} coverUrl
*/
coverUrl() {
return this.loaded.cover.then(() => {
if (!this.cover) {
return null;
}
if (this.archived) {
return this.archive.createUrl(this.cover);
} else {
return this.cover;
}
});
}
/**
* Load replacement urls
* @private
* @return {Promise} completed loading urls
*/
replacements() {
this.spine.hooks.serialize.register((output, section) => {
section.output = this.resources.substitute(output, section.url);
});
return this.resources.replacements().
then(() => {
return this.resources.replaceCss();
});
}
/**
* Find a DOM Range for a given CFI Range
* @param {EpubCFI} cfiRange a epub cfi range
* @return {Promise}
*/
getRange(cfiRange) {
var cfi = new EpubCFI(cfiRange);
var item = this.spine.get(cfi.spinePos);
var _request = this.load.bind(this);
if (!item) {
return new Promise((resolve, reject) => {
reject("CFI could not be found");
});
}
return item.load(_request).then(function (contents) {
var range = cfi.toRange(item.document);
return range;
});
}
/**
* Generates the Book Key using the identifier in the manifest or other string provided
* @param {string} [identifier] to use instead of metadata identifier
* @return {string} key
*/
key(identifier) {
var ident = identifier || this.packaging.metadata.identifier || this.url.filename;
return `epubjs:${EPUBJS_VERSION}:${ident}`;
}
/**
* Destroy the Book and all associated objects
*/
destroy() {
this.opened = undefined;
this.loading = undefined;
this.loaded = undefined;
this.ready = undefined;
this.isOpen = false;
this.isRendered = false;
this.spine && this.spine.destroy();
this.locations && this.locations.destroy();
this.pageList && this.pageList.destroy();
this.archive && this.archive.destroy();
this.resources && this.resources.destroy();
this.container && this.container.destroy();
this.packaging && this.packaging.destroy();
this.rendition && this.rendition.destroy();
this.displayOptions && this.displayOptions.destroy();
this.spine = undefined;
this.locations = undefined;
this.pageList = undefined;
this.archive = undefined;
this.resources = undefined;
this.container = undefined;
this.packaging = undefined;
this.rendition = undefined;
this.navigation = undefined;
this.url = undefined;
this.path = undefined;
this.archived = false;
}
}
//-- Enable binding events to book
EventEmitter(Book.prototype);
export default Book;

View file

@ -1,35 +0,0 @@
import Book from "./book";
import Rendition from "./rendition";
import CFI from "./epubcfi";
import Contents from "./contents";
import * as utils from "./utils/core";
import { EPUBJS_VERSION } from "./utils/constants";
import IframeView from "./managers/views/iframe";
import DefaultViewManager from "./managers/default";
import ContinuousViewManager from "./managers/continuous";
/**
* Creates a new Book
* @param {string|ArrayBuffer} url URL, Path or ArrayBuffer
* @param {object} options to pass to the book
* @returns {Book} a new Book object
* @example ePub("/path/to/book.epub", {})
*/
function ePub(url, options) {
return new Book(url, options);
}
ePub.VERSION = EPUBJS_VERSION;
if (typeof(global) !== "undefined") {
global.EPUBJS_VERSION = EPUBJS_VERSION;
}
ePub.Book = Book;
ePub.Rendition = Rendition;
ePub.Contents = Contents;
ePub.CFI = CFI;
ePub.utils = utils;
export default ePub;

View file

@ -1,5 +1,5 @@
import path from "path-webpack";
import {qs} from "./utils/core";
import { qs } from "../utils/core.js";
import { directory } from "../utils/url.js";
/**
* Handles Parsing and Accessing an Epub Container
@ -8,9 +8,9 @@ import {qs} from "./utils/core";
*/
class Container {
constructor(containerDocument) {
this.packagePath = '';
this.directory = '';
this.encoding = '';
this.packagePath = "";
this.directory = "";
this.encoding = "";
if (containerDocument) {
this.parse(containerDocument);
@ -36,7 +36,7 @@ class Container {
}
this.packagePath = rootfile.getAttribute("full-path");
this.directory = path.dirname(this.packagePath);
this.directory = directory(this.packagePath);
this.encoding = containerDocument.xmlEncoding;
}

View file

@ -1,4 +1,4 @@
import {qs, qsa } from "./utils/core";
import { qs, qsa } from "../utils/core.js";
/**
* Open DisplayOptions Format Parser

230
src/epub/epub.js Normal file
View file

@ -0,0 +1,230 @@
import EventEmitter from "../utils/eventemitter.js";
import Publication from "../publication/publication.js";
import Locations from "./locations.js";
import Container from "./container.js";
import Packaging from "./packaging.js";
import Navigation from "./navigation.js";
import PageList from "./pagelist.js";
import Spine from "./spine.js";
import { extension } from "../utils/url.js";
import EpubCFI from "../utils/epubcfi.js";
const CONTAINER_PATH = "META-INF/container.xml";
const IBOOKS_DISPLAY_OPTIONS_PATH = "META-INF/com.apple.ibooks.display-options.xml";
const INPUT_TYPE = {
EPUB: "epub",
OPF: "opf",
DIRECTORY: "directory"
};
class Epub extends Publication {
constructor(url, options) {
super();
if (url) {
this.opened = this.open(url);
}
}
/**
* Determine the type of they input passed to open
* @private
* @param {string} input
* @return {string} directory | epub | opf
*/
determineType(input) {
const ext = extension(input);
if (!ext) {
return INPUT_TYPE.DIRECTORY;
}
if (ext === "epub") {
return INPUT_TYPE.EPUB;
}
if (ext === "opf") {
return INPUT_TYPE.OPF;
}
}
/**
* Open the epub container
* @private
* @param {string} url
* @return {string} packagePath
*/
async loadContainer(url) {
const xml = await this.load(url);
this.container = new Container(xml);
return this.container.packagePath;
}
/**
* Open the Open Packaging Format Xml
* @private
* @param {string} url
* @return {Promise}
*/
async loadPackaging(url) {
const xml = await this.load(url);
this.packaging = new Packaging(xml);
return this.packaging;
}
/**
* Load Navigation and PageList from package
* @private
* @param {document} opf XML Document
*/
async loadNavigation(opf) {
let navPath = opf.navPath || opf.ncxPath;
if (!navPath) {
return {
toc: undefined,
landmarks: undefined,
pageList: undefined,
locations: undefined
}
}
const xml = await this.load(navPath, "xml");
const navigation = new Navigation(xml, this.resolve(navPath));
const pagelist = new PageList(xml);
return {
toc: navigation.toc,
landmarks: navigation.landmarks,
pageList: pagelist.pages,
locations: pagelist.locations
}
}
async loadDisplayOptions(packaging) {
let displayOptionsXml;
if (packaging.metadata.layout === "") {
displayOptionsXml = await this.load(IBOOKS_DISPLAY_OPTIONS_PATH).catch((err) => {
return undefined;
});
}
return displayOptionsXml;
}
loadSections(packaging) {
let spine = new Spine(packaging);
return {
readingOrder: spine.readingOrder,
unordered: spine.resources
}
}
loadResources(packaging) {
let resources = [];
for (const r in packaging.manifest) {
const resource = packaging.manifest[r];
if (resource.type !== "application/xhtml+xml" &&
resource.type !== "text/html") {
resource.url = resource.href;
resources.push(resource);
}
}
return resources;
}
loadMetadata(packaging) {
return packaging.metadata;
}
/**
* Unpack the contents of the Epub
* @private
* @param {document} packageXml XML Document
*/
async unpack(packaging) {
this.package = packaging;
this.metadata = this.loadMetadata(packaging);
const resources = this.loadResources(packaging);
const { readingOrder, unordered } = this.loadSections(packaging);
this.spine = readingOrder;
this.resources = [...unordered, ...resources];
const { toc, landmarks, pageList, locations } = await this.loadNavigation(packaging);
this.navigation = toc;
this.landmarks = landmarks;
this.pagelist = pageList;
this.locations = locations;
this.displayOptions = await this.loadDisplayOptions(packaging);
return this;
}
async open(url, what) {
const type = what || this.determineType(url);
let packaging;
this.url = url;
if (type === INPUT_TYPE.EPUB) {
throw new Error("Epub must be unarchived");
}
if (type === INPUT_TYPE.DIRECTORY) {
const container = await this.loadContainer(CONTAINER_PATH);
packaging = await this.loadPackaging(container.packagePath);
} else {
packaging = await this.loadPackaging(url);
}
return this.unpack(packaging);
}
// TODO: move to rendition?
generateLocations(breakPoint) {
let locations = new Locations();
return locations.generate(this.sections, breakPoint)
.then((locations) => {
this.locations = locations;
return locations;
})
}
get spine() {
return this.sections;
}
set spine(items) {
return this.sections = items;
}
get toc() {
return this.navigation;
}
set toc(items) {
return this.navigation = items;
}
toJSON() {
}
/**
* Destroy the Epub and all associated objects
*/
destroy() {
}
}
EventEmitter(Epub.prototype);
export default Epub;

View file

@ -1,8 +1,8 @@
import {qs, sprint, locationOf, defer} from "./utils/core";
import Queue from "./utils/queue";
import EpubCFI from "./epubcfi";
import { EVENTS } from "./utils/constants";
import EventEmitter from "event-emitter";
import { qs, sprint, locationOf, defer } from "../utils/core.js";
import Queue from "../utils/queue.js";
import EpubCFI from "../utils/epubcfi.js";
import { EVENTS } from "../utils/constants.js";
import EventEmitter from "../utils/eventemitter.js";
/**
* Find Locations for a Book
@ -11,7 +11,7 @@ import EventEmitter from "event-emitter";
* @param {number} [pause=100]
*/
class Locations {
constructor(spine, request, pause) {
constructor(request, pause) {
this.spine = spine;
this.request = request;
this.pause = pause || 100;
@ -39,7 +39,7 @@ class Locations {
* @param {int} chars how many chars to split on
* @return {Promise<Array<string>>} locations
*/
generate(chars) {
generate(spine, chars) {
if (chars) {
this.break = chars;
@ -80,7 +80,7 @@ class Locations {
return section.load(this.request)
.then(function(contents) {
var completed = new defer();
var locations = this.parse(contents, section.cfiBase);
var locations = this.parse(contents, section.canonical);
this._locations = this._locations.concat(locations);
section.unload();
@ -417,10 +417,11 @@ class Locations {
/**
* Save locations to JSON
* @alias toJSON
* @return {json}
*/
save(){
return JSON.stringify(this._locations);
return this.toJSON();
}
getCurrent(){
@ -475,6 +476,22 @@ class Locations {
return this._locations.length;
}
/**
* Export locations as an Array
* @return {array}
*/
toArray() {
return this._locations;
}
/**
* Export locations as JSON
* @return {json}
*/
toJSON() {
return JSON.stringify(this._locations);
}
destroy () {
this.spine = undefined;
this.request = undefined;

View file

@ -1,4 +1,11 @@
import {qs, qsa, querySelectorByType, filterChildren, getParentByTagName} from "./utils/core";
import {
qs,
qsa,
querySelectorByType,
filterChildren,
getParentByTagName,
uuid
} from "../utils/core.js";
/**
* Navigation Parser
@ -68,8 +75,8 @@ class Navigation {
this.length++;
if (item.subitems.length) {
this.unpack(item.subitems);
if (item.children.length) {
this.unpack(item.children);
}
}
@ -198,25 +205,25 @@ class Navigation {
return;
}
let src = content.getAttribute("href") || "";
if (!id) {
id = src;
id = 'epubjs-autogen-toc-id-' + uuid();
item.setAttribute('id', id);
}
let text = content.textContent || "";
let subitems = [];
let href = content.getAttribute("href") || "";
let name = content.textContent || "";
let children = [];
let nested = filterChildren(item, "ol", true);
if (nested) {
subitems = this.parseNavList(nested, id);
children = this.parseNavList(nested, id);
}
return {
"id": id,
"href": src,
"label": text,
"subitems" : subitems,
"parent" : parent
id,
href,
name,
children,
parent
};
}
@ -227,12 +234,12 @@ class Navigation {
* @return {array} landmarks list
*/
parseLandmarks(navHtml){
var navElement = querySelectorByType(navHtml, "nav", "landmarks");
var navItems = navElement ? qsa(navElement, "li") : [];
var length = navItems.length;
var i;
var list = [];
var item;
let navElement = querySelectorByType(navHtml, "nav", "landmarks");
let navItems = navElement ? qsa(navElement, "li") : [];
let length = navItems.length;
let i;
let list = [];
let item;
if(!navItems || length === 0) return list;
@ -262,12 +269,12 @@ class Navigation {
let type = content.getAttributeNS("http://www.idpf.org/2007/ops", "type") || undefined;
let href = content.getAttribute("href") || "";
let text = content.textContent || "";
let name = content.textContent || "";
return {
"href": href,
"label": text,
"type" : type
href,
name,
type
};
}
@ -278,12 +285,12 @@ class Navigation {
* @return {array} navigation list
*/
parseNcx(tocXml){
var navPoints = qsa(tocXml, "navPoint");
var length = navPoints.length;
var i;
var toc = {};
var list = [];
var item, parent;
let navPoints = qsa(tocXml, "navPoint");
let length = navPoints.length;
let i;
let toc = {};
let list = [];
let item, parent;
if(!navPoints || length === 0) return list;
@ -294,7 +301,7 @@ class Navigation {
list.push(item);
} else {
parent = toc[item.parent];
parent.subitems.push(item);
parent.children.push(item);
}
}
@ -310,10 +317,10 @@ class Navigation {
ncxItem(item){
var id = item.getAttribute("id") || false,
content = qs(item, "content"),
src = content.getAttribute("src"),
href = content.getAttribute("src"),
navLabel = qs(item, "navLabel"),
text = navLabel.textContent ? navLabel.textContent : "",
subitems = [],
name = navLabel.textContent ? navLabel.textContent : "",
children = [],
parentNode = item.parentNode,
parent;
@ -321,29 +328,20 @@ class Navigation {
parent = parentNode.getAttribute("id");
}
if (!id) {
id = 'epubjs-autogen-toc-id-' + uuid();
item.setAttribute('id', id);
}
return {
"id": id,
"href": src,
"label": text,
"subitems" : subitems,
"parent" : parent
id,
href,
name,
children,
parent
};
}
/**
* Load Spine Items
* @param {object} json the items to be loaded
* @return {Array} navItems
*/
load(json) {
return json.map(item => {
item.label = item.title;
item.subitems = item.children ? this.load(item.children) : [];
return item;
});
}
/**
* forEach pass through
* @param {Function} fn function to run on each item
@ -352,6 +350,43 @@ class Navigation {
forEach(fn) {
return this.toc.forEach(fn);
}
/**
* Get an Array of all Table of Contents Items
*/
getTocArray(resolver) {
return this.toc.map((item) => {
let url = resolver ? resolver(item.href) : item.href;
let obj = {
href: url,
name: item.name
};
if (item.children.length) {
obj.children = item.children;
}
return obj;
});
}
/**
* Get an Array of all landmarks
*/
getLandmarksArray(resolver) {
return this.landmarks.map((item) => {
let url = resolver ? resolver(item.href) : item.href;
let obj = {
href: url,
name: item.name,
type: item.type
};
return obj;
});
}
}
export default Navigation;

View file

@ -1,4 +1,4 @@
import {qs, qsa, qsp, indexOfElementNode} from "./utils/core";
import { qs, qsa, qsp, indexOfElementNode } from "../utils/core.js";
/**
* Open Packaging Format Parser
@ -8,9 +8,9 @@ import {qs, qsa, qsp, indexOfElementNode} from "./utils/core";
class Packaging {
constructor(packageDocument) {
this.manifest = {};
this.navPath = '';
this.ncxPath = '';
this.coverPath = '';
this.navPath = "";
this.ncxPath = "";
this.coverPath = "";
this.spineNodeIndex = 0;
this.spine = [];
this.metadata = {};
@ -26,23 +26,21 @@ class Packaging {
* @return {object} parsed package parts
*/
parse(packageDocument){
var metadataNode, manifestNode, spineNode;
if(!packageDocument) {
throw new Error("Package File Not Found");
}
metadataNode = qs(packageDocument, "metadata");
const metadataNode = qs(packageDocument, "metadata");
if(!metadataNode) {
throw new Error("No Metadata Found");
}
manifestNode = qs(packageDocument, "manifest");
const manifestNode = qs(packageDocument, "manifest");
if(!manifestNode) {
throw new Error("No Manifest Found");
}
spineNode = qs(packageDocument, "spine");
const spineNode = qs(packageDocument, "spine");
if(!spineNode) {
throw new Error("No Spine Found");
}
@ -53,6 +51,7 @@ class Packaging {
this.coverPath = this.findCoverPath(packageDocument);
this.spineNodeIndex = indexOfElementNode(spineNode);
this.manifestNodeIndex = indexOfElementNode(manifestNode);
this.spine = this.parseSpine(spineNode, this.manifest);
@ -68,7 +67,8 @@ class Packaging {
"navPath" : this.navPath,
"ncxPath" : this.ncxPath,
"coverPath": this.coverPath,
"spineNodeIndex" : this.spineNodeIndex
"spineNodeIndex" : this.spineNodeIndex,
"manifestNodeIndex" : this.manifestNodeIndex,
};
}
@ -79,7 +79,7 @@ class Packaging {
* @return {object} metadata
*/
parseMetadata(xml){
var metadata = {};
const metadata = {};
metadata.title = this.getElementText(xml, "title");
metadata.creator = this.getElementText(xml, "creator");
@ -112,7 +112,7 @@ class Packaging {
* @param {node} manifestXml
* @return {object} manifest
*/
parseManifest(manifestXml){
parseManifest(manifestXml) {
var manifest = {};
//-- Turn items into an array
@ -121,19 +121,19 @@ class Packaging {
var items = Array.prototype.slice.call(selected);
//-- Create an object with the id as key
items.forEach(function(item){
items.forEach(function (item) {
var id = item.getAttribute("id"),
href = item.getAttribute("href") || "",
type = item.getAttribute("media-type") || "",
overlay = item.getAttribute("media-overlay") || "",
properties = item.getAttribute("properties") || "";
href = item.getAttribute("href") || "",
type = item.getAttribute("media-type") || "",
overlay = item.getAttribute("media-overlay") || "",
properties = item.getAttribute("properties") || "";
manifest[id] = {
"href" : href,
"href": href,
// "url" : href,
"type" : type,
"overlay" : overlay,
"properties" : properties.length ? properties.split(" ") : []
"type": type,
"overlay": overlay,
"properties": properties.length ? properties.split(" ") : []
};
});
@ -150,31 +150,24 @@ class Packaging {
* @return {object} spine
*/
parseSpine(spineXml, manifest){
var spine = [];
const spine = [];
var selected = qsa(spineXml, "itemref");
var items = Array.prototype.slice.call(selected);
const selected = qsa(spineXml, "itemref");
const items = Array.prototype.slice.call(selected);
// var epubcfi = new EpubCFI();
//-- Add to array to maintain ordering and cross reference with manifest
items.forEach(function(item, index){
var idref = item.getAttribute("idref");
// var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id);
var props = item.getAttribute("properties") || "";
var propArray = props.length ? props.split(" ") : [];
// var manifestProps = manifest[Id].properties;
// var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : [];
const idref = item.getAttribute("idref");
const props = item.getAttribute("properties") || "";
const propArray = props.length ? props.split(" ") : [];
var itemref = {
"id" : item.getAttribute("id"),
const itemref = {
"idref" : idref,
"linear" : item.getAttribute("linear") || "yes",
"properties" : propArray,
// "href" : manifest[Id].href,
// "url" : manifest[Id].url,
"index" : index
// "cfiBase" : cfiBase
};
spine.push(itemref);
});
@ -189,12 +182,12 @@ class Packaging {
* @return {string} Unique Identifier text
*/
findUniqueIdentifier(packageXml){
var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier");
if (! uniqueIdentifierId) {
const uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier");
if (!uniqueIdentifierId) {
return "";
}
var identifier = packageXml.getElementById(uniqueIdentifierId);
if (! identifier) {
const identifier = packageXml.getElementById(uniqueIdentifierId);
if (!identifier) {
return "";
}
@ -214,8 +207,7 @@ class Packaging {
findNavPath(manifestNode){
// Find item with property "nav"
// Should catch nav regardless of order
// var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']");
var node = qsp(manifestNode, "item", {"properties":"nav"});
const node = qsp(manifestNode, "item", {"properties":"nav"});
return node ? node.getAttribute("href") : false;
}
@ -228,17 +220,14 @@ class Packaging {
* @return {string}
*/
findNcxPath(manifestNode, spineNode){
// var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']");
var node = qsp(manifestNode, "item", {"media-type":"application/x-dtbncx+xml"});
var tocId;
let node = qsp(manifestNode, "item", {"media-type":"application/x-dtbncx+xml"});
// If we can't find the toc by media-type then try to look for id of the item in the spine attributes as
// according to http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4.1.2,
// "The item that describes the NCX must be referenced by the spine toc attribute."
if (!node) {
tocId = spineNode.getAttribute("toc");
const tocId = spineNode.getAttribute("toc");
if(tocId) {
// node = manifestNode.querySelector("item[id='" + tocId + "']");
node = manifestNode.querySelector(`#${tocId}`);
}
}
@ -255,24 +244,20 @@ class Packaging {
* @return {string} href
*/
findCoverPath(packageXml){
var pkg = qs(packageXml, "package");
var epubVersion = pkg.getAttribute("version");
// Try parsing cover with epub 3.
// var node = packageXml.querySelector("item[properties='cover-image']");
var node = qsp(packageXml, "item", {"properties":"cover-image"});
if (node) return node.getAttribute("href");
const node = qsp(packageXml, "item", {"properties":"cover-image"});
if (node) {
return node.getAttribute("href");
}
// Fallback to epub 2.
var metaCover = qsp(packageXml, "meta", {"name":"cover"});
const metaCover = qsp(packageXml, "meta", {"name":"cover"});
if (metaCover) {
var coverId = metaCover.getAttribute("content");
// var cover = packageXml.querySelector("item[id='" + coverId + "']");
var cover = packageXml.getElementById(coverId);
const coverId = metaCover.getAttribute("content");
const cover = packageXml.getElementById(coverId);
return cover ? cover.getAttribute("href") : "";
}
else {
} else {
return false;
}
}
@ -285,19 +270,17 @@ class Packaging {
* @return {string} text
*/
getElementText(xml, tag){
var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag);
var el;
const found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag);
if(!found || found.length === 0) return "";
el = found[0];
const el = found[0];
if(el.childNodes.length){
return el.childNodes[0].nodeValue;
}
return "";
}
/**
@ -308,7 +291,7 @@ class Packaging {
* @return {string} text
*/
getPropertyText(xml, property){
var el = qsp(xml, "meta", {"property":property});
const el = qsp(xml, "meta", {"property":property});
if(el && el.childNodes.length){
return el.childNodes[0].nodeValue;
@ -317,48 +300,6 @@ class Packaging {
return "";
}
/**
* Load JSON Manifest
* @param {document} packageDocument OPF XML
* @return {object} parsed package parts
*/
load(json) {
this.metadata = json.metadata;
let spine = json.readingOrder || json.spine;
this.spine = spine.map((item, index) =>{
item.index = index;
item.linear = item.linear || "yes";
return item;
});
json.resources.forEach((item, index) => {
this.manifest[index] = item;
if (item.rel && item.rel[0] === "cover") {
this.coverPath = item.href;
}
});
this.spineNodeIndex = 0;
this.toc = json.toc.map((item, index) =>{
item.label = item.title;
return item;
});
return {
"metadata" : this.metadata,
"spine" : this.spine,
"manifest" : this.manifest,
"navPath" : this.navPath,
"ncxPath" : this.ncxPath,
"coverPath": this.coverPath,
"spineNodeIndex" : this.spineNodeIndex,
"toc" : this.toc
};
}
destroy() {
this.manifest = undefined;
this.navPath = undefined;

View file

@ -1,11 +1,11 @@
import EpubCFI from "./epubcfi";
import EpubCFI from "../utils/epubcfi.js";
import {
qs,
qsa,
querySelectorByType,
indexOfSorted,
locationOf
} from "./utils/core";
} from "../utils/core.js";
/**
* Page List Parser
@ -256,6 +256,22 @@ class PageList {
return percentage;
}
/**
* Export pages as an Array
* @return {array}
*/
toArray() {
return this.locations;
}
/**
* Export pages as JSON
* @return {json}
*/
toJSON() {
return JSON.stringify(this.locations);
}
/**
* Destroy
*/

69
src/epub/spine.js Normal file
View file

@ -0,0 +1,69 @@
import EpubCFI from "../utils/epubcfi.js";
/**
* A collection of Spine Items
*/
class Spine {
constructor(packaging) {
this.items = undefined;
this.manifest = undefined;
this.spineNodeIndex = undefined;
this.baseUrl = undefined;
this.length = 0;
this.readingOrder = [];
this.resources = [];
this.epubcfi = new EpubCFI();
if (packaging) {
this.unpack(packaging);
}
}
/**
* Unpack items from a opf into spine items
* @param {Packaging} packaging
*/
unpack(packaging) {
this.items = packaging.spine;
this.manifest = packaging.manifest;
this.spineNodeIndex = packaging.spineNodeIndex;
this.baseUrl = packaging.baseUrl || packaging.basePath || "";
this.length = this.items.length;
this.items.forEach( (item, index) => {
let manifestItem = this.manifest[item.idref];
item.id = item.idref;
item.canonical = this.epubcfi.generateChapterComponent(this.spineNodeIndex, item.index, item.idref);
item.cfiPos = index;
if(manifestItem) {
item.url = manifestItem.href;
if(manifestItem.properties.length){
item.properties.push.apply(item.properties, manifestItem.properties);
}
}
if (item.linear === "yes") {
this.readingOrder.push(item);
} else {
this.resources.push(item);
}
});
}
destroy() {
this.items = undefined;
this.manifest = undefined;
this.spineNodeIndex = undefined;
this.baseUrl = undefined;
this.length = undefined;
this.epubcfi = undefined;
this.readingOrder = undefined;
}
}
export default Spine;

View file

@ -1,13 +1,15 @@
import Book from "./book";
import EpubCFI from "./epubcfi";
import Rendition from "./rendition";
import Contents from "./contents";
import Layout from "./layout";
import ePub from "./epub";
export default ePub;
import Publication from "./publication/publication.js";
import EpubCFI from "./utils/epubcfi.js";
import Rendition from "./rendition/rendition.js";
import Contents from "./rendition/contents.js";
import Layout from "./rendition/layout.js";
import Epub from "./epub/epub.js";
import Manifest from "./manifest/manifest.js";
// import ePub from "./epub.js";
export {
Book,
Publication,
Epub,
Manifest,
EpubCFI,
Rendition,
Contents,

View file

@ -1,8 +1,8 @@
import {extend, defer, requestAnimationFrame} from "../../utils/core";
import DefaultViewManager from "../default";
import Snap from "../helpers/snap";
import { EVENTS } from "../../utils/constants";
import debounce from "lodash/debounce";
import { extend, defer, requestAnimationFrame, debounce, nextSection, prevSection } from "../../utils/core.js";
import DefaultViewManager from "../default/default.js";
import Snap from "../helpers/snap.js";
import { EVENTS } from "../../utils/constants.js";
// import debounce from "lodash/debounce";
class ContinuousViewManager extends DefaultViewManager {
constructor(options) {
@ -40,7 +40,9 @@ class ContinuousViewManager extends DefaultViewManager {
width: 0,
height: 0,
forceEvenPages: false,
allowScriptedContent: this.settings.allowScriptedContent
allowScriptedContent: this.settings.allowScriptedContent,
hooks: this.hooks,
request: this.request
};
this.scrollTop = 0;
@ -280,7 +282,7 @@ class ContinuousViewManager extends DefaultViewManager {
let prepend = () => {
let first = this.views.first();
let prev = first && first.section.prev();
let prev = first && prevSection(first.section, this.sections);
if(prev) {
newViews.push(this.prepend(prev));
@ -289,8 +291,8 @@ class ContinuousViewManager extends DefaultViewManager {
let append = () => {
let last = this.views.last();
let next = last && last.section.next();
let next = last && nextSection(last.section, this.sections);
if(next) {
newViews.push(this.append(next));
}

View file

@ -1,11 +1,11 @@
import EventEmitter from "event-emitter";
import {extend, defer, windowBounds, isNumber} from "../../utils/core";
import scrollType from "../../utils/scrolltype";
import Mapping from "../../mapping";
import Queue from "../../utils/queue";
import Stage from "../helpers/stage";
import Views from "../helpers/views";
import { EVENTS } from "../../utils/constants";
import EventEmitter from "../../utils/eventemitter.js";
import { extend, defer, windowBounds, isNumber, nextSection, prevSection} from "../../utils/core.js";
import scrollType from "../../utils/scrolltype.js";
import Mapping from "../../rendition/mapping.js";
import Queue from "../../utils/queue.js";
import Stage from "../helpers/stage.js";
import Views from "../helpers/views.js";
import { EVENTS } from "../../utils/constants.js";
class DefaultViewManager {
constructor(options) {
@ -13,8 +13,9 @@ class DefaultViewManager {
this.name = "default";
this.optsSettings = options.settings;
this.View = options.view;
this.sections = options.sections;
this.request = options.request;
this.renditionQueue = options.queue;
this.hooks = options.hooks;
this.q = new Queue(this);
this.settings = extend(this.settings || {}, {
@ -27,7 +28,7 @@ class DefaultViewManager {
flow: "scrolled",
ignoreClass: "",
fullsize: undefined,
allowScriptedContent: false
allowScriptedContent: false,
});
extend(this.settings, options.settings || {});
@ -37,11 +38,12 @@ class DefaultViewManager {
axis: this.settings.axis,
flow: this.settings.flow,
layout: this.layout,
method: this.settings.method, // srcdoc, blobUrl, write
width: 0,
height: 0,
forceEvenPages: true,
allowScriptedContent: this.settings.allowScriptedContent
allowScriptedContent: this.settings.allowScriptedContent,
hooks: this.hooks,
request: this.request
};
this.rendered = false;
@ -74,7 +76,8 @@ class DefaultViewManager {
hidden: this.settings.hidden,
axis: this.settings.axis,
fullsize: this.settings.fullsize,
direction: this.settings.direction
direction: this.settings.direction,
scale: this.settings.scale
});
this.stage.attachTo(element);
@ -138,7 +141,9 @@ class DefaultViewManager {
scroller = window;
}
scroller.removeEventListener("scroll", this._onScroll);
if (scroller) {
scroller.removeEventListener("scroll", this._onScroll);
}
this._onScroll = undefined;
}
@ -151,7 +156,7 @@ class DefaultViewManager {
this.removeEventListeners();
this.stage.destroy();
this.stage && this.stage.destroy();
this.rendered = false;
@ -246,7 +251,7 @@ class DefaultViewManager {
// First page (cover) should stand alone for pre-paginated books
return;
}
next = section.next();
next = nextSection(section, this.sections);
if (next && !next.properties.includes("page-spread-left")) {
return action.call(this, next);
}
@ -259,7 +264,7 @@ class DefaultViewManager {
var displayed = displaying.promise;
// Check if moving to target is needed
if (target === section.href || isNumber(target)) {
if (target === section.url || isNumber(target)) {
target = undefined;
}
@ -463,7 +468,7 @@ class DefaultViewManager {
if(left <= this.container.scrollWidth) {
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
next = nextSection(this.views.last().section, this.sections);
}
} else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
@ -475,7 +480,7 @@ class DefaultViewManager {
if (left > 0) {
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
next = nextSection(this.views.last().section, this.sections);
}
} else {
left = this.container.scrollLeft + ( this.layout.delta * -1 );
@ -483,7 +488,7 @@ class DefaultViewManager {
if (left > this.container.scrollWidth * -1){
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
next = nextSection(this.views.last().section, this.sections);
}
}
@ -496,11 +501,11 @@ class DefaultViewManager {
if(top < this.container.scrollHeight) {
this.scrollBy(0, this.layout.height, true);
} else {
next = this.views.last().section.next();
next = nextSection(this.views.last().section, this.sections);
}
} else {
next = this.views.last().section.next();
next = nextSection(this.views.last().section, this.sections);
}
if(next) {
@ -552,7 +557,7 @@ class DefaultViewManager {
if(left > 0) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
prev = prevSection(this.views.first().section, this.sections);
}
} else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
@ -565,7 +570,7 @@ class DefaultViewManager {
if (left < this.container.scrollWidth) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
prev = prevSection(this.views.first().section, this.sections);
}
}
else{
@ -574,7 +579,7 @@ class DefaultViewManager {
if (left < 0) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
prev = prevSection(this.views.first().section, this.sections);
}
}
@ -587,12 +592,12 @@ class DefaultViewManager {
if(top > 0) {
this.scrollBy(0, -(this.layout.height), true);
} else {
prev = this.views.first().section.prev();
prev = prevSection(this.views.first().section, this.sections);
}
} else {
prev = this.views.first().section.prev();
prev = prevSection(this.views.first().section, this.sections);
}
@ -682,7 +687,7 @@ class DefaultViewManager {
}
let sections = visible.map((view) => {
let {index, href} = view.section;
let {index, url} = view.section;
let position = view.position();
let width = view.width();
let height = view.height();
@ -721,11 +726,11 @@ class DefaultViewManager {
pages.push(pg);
}
let mapping = this.mapping.page(view.contents, view.section.cfiBase, startPos, endPos);
let mapping = this.mapping.page(view.contents, view.section.canonical, startPos, endPos);
return {
index,
href,
url,
pages,
totalPages,
mapping
@ -747,7 +752,7 @@ class DefaultViewManager {
}
let sections = visible.map((view) => {
let {index, href} = view.section;
let {index, url} = view.section;
let offset;
let position = view.position();
let width = view.width();
@ -771,7 +776,7 @@ class DefaultViewManager {
used += pageWidth;
let mapping = this.mapping.page(view.contents, view.section.cfiBase, start, end);
let mapping = this.mapping.page(view.contents, view.section.canonical, start, end);
let totalPages = this.layout.count(width).pages;
let startPage = Math.floor(start / this.layout.pageWidth);
@ -799,7 +804,7 @@ class DefaultViewManager {
return {
index,
href,
url,
pages,
totalPages,
mapping
@ -1067,6 +1072,14 @@ class DefaultViewManager {
isRendered() {
return this.rendered;
}
scale(s) {
this.settings.scale = s;
if (this.stage) {
this.stage.scale(s);
}
}
}
//-- Enable binding events to Manager

View file

@ -1,6 +1,6 @@
import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core";
import { EVENTS, DOM_EVENTS } from "../../utils/constants";
import EventEmitter from "event-emitter";
import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core.js";
import { EVENTS, DOM_EVENTS } from "../../utils/constants.js";
import EventEmitter from "../../utils/eventemitter.js";
// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
const PI_D2 = (Math.PI / 2);

View file

@ -1,5 +1,5 @@
import {uuid, isNumber, isElement, windowBounds, extend} from "../../utils/core";
import throttle from 'lodash/throttle'
import {uuid, isNumber, isElement, windowBounds, extend, throttle} from "../../utils/core.js";
// import throttle from 'lodash/throttle'
class Stage {
constructor(_options) {

View file

@ -1,9 +1,21 @@
import EventEmitter from "event-emitter";
import {extend, borders, uuid, isNumber, bounds, defer, createBlobUrl, revokeBlobUrl} from "../../utils/core";
import EpubCFI from "../../epubcfi";
import Contents from "../../contents";
import { EVENTS } from "../../utils/constants";
import { Pane, Highlight, Underline } from "marks-pane";
import EventEmitter from "../../utils/eventemitter.js";
import {
extend,
borders,
uuid,
isNumber,
bounds,
defer,
createBlobUrl,
revokeBlobUrl,
serialize
} from "../../utils/core.js";
import EpubCFI from "../../utils/epubcfi.js";
import Contents from "../../rendition/contents.js";
import { EVENTS } from "../../utils/constants.js";
import { Pane, Highlight, Underline } from "../../marks/marks.js";
import request from "../../utils/request.js";
import Hook from "../../utils/hook.js";
class IframeView {
constructor(section, options) {
@ -15,11 +27,26 @@ class IframeView {
height: 0,
layout: undefined,
globalLayoutProperties: {},
method: undefined,
forceRight: false,
allowScriptedContent: false
allowScriptedContent: false,
request: undefined,
hooks: undefined
}, options || {});
if (this.settings.request) {
this.request = this.settings.request;
} else {
this.request = request;
}
if (this.settings.hooks) {
this.hooks = this.settings.hooks;
} else {
this.hooks = {};
this.hooks.serialize = new Hook(this);
this.hooks.document = new Hook(this);
}
this.id = "epubjs-view-" + uuid();
this.section = section;
this.index = section.index;
@ -110,6 +137,7 @@ class IframeView {
this.element.setAttribute("ref", this.index);
this.element.appendChild(this.iframe);
this.added = true;
this.elementBounds = bounds(this.element);
@ -129,31 +157,29 @@ class IframeView {
this.supportsSrcdoc = false;
}
if (!this.settings.method) {
this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write";
}
return this.iframe;
}
render(request, show) {
render(requestMethod, show) {
let sectionDocument;
// view.onLayout = this.layout.format.bind(this.layout);
this.create();
// Fit to size of the container, apply padding
this.size();
if(!this.sectionRender) {
this.sectionRender = this.section.render(request);
}
const loader = requestMethod || this.request;
sectionDocument = loader(this.section.url).catch((e) => {
this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
return new Promise((resolve, reject) => {
reject(e);
});
});
// Render Chain
return this.sectionRender
.then(function(contents){
return this.load(contents);
}.bind(this))
.then(function(){
return this.load(sectionDocument, this.section.encoding)
.then(() => {
// find and report the writingMode axis
let writingMode = this.contents.writingMode();
@ -193,15 +219,15 @@ class IframeView {
resolve();
});
}.bind(this), function(e){
}, (e) => {
this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
return new Promise((resolve, reject) => {
reject(e);
});
}.bind(this))
.then(function() {
})
.then(() => {
this.emit(EVENTS.VIEWS.RENDERED, this.section);
}.bind(this));
});
}
@ -228,7 +254,7 @@ class IframeView {
this.lock("both", width, height);
} else if(this.settings.axis === "horizontal") {
this.lock("height", width, height);
} else {
} else {
this.lock("width", width, height);
}
@ -377,7 +403,7 @@ class IframeView {
}
load(contents) {
load(sectionDocument, mimeType="text/html") {
var loading = new defer();
var loaded = loading.promise;
@ -392,58 +418,28 @@ class IframeView {
}.bind(this);
if (this.settings.method === "blobUrl") {
this.blobUrl = createBlobUrl(contents, "application/xhtml+xml");
sectionDocument.then(async (r) => {
await this.hooks.document.trigger(r, this.section, this);
let text = serialize(r);
await this.hooks.serialize.trigger(text, this);
this.blobUrl = createBlobUrl(text, mimeType); //"application/xhtml+xml"
this.iframe.src = this.blobUrl;
this.element.appendChild(this.iframe);
} else if(this.settings.method === "srcdoc"){
this.iframe.srcdoc = contents;
this.element.appendChild(this.iframe);
} else {
this.element.appendChild(this.iframe);
this.document = this.iframe.contentDocument;
if(!this.document) {
loading.reject(new Error("No Document Available"));
return loaded;
}
this.iframe.contentDocument.open();
// For Cordova windows platform
if(window.MSApp && MSApp.execUnsafeLocalFunction) {
var outerThis = this;
MSApp.execUnsafeLocalFunction(function () {
outerThis.iframe.contentDocument.write(contents);
});
} else {
this.iframe.contentDocument.write(contents);
}
this.iframe.contentDocument.close();
}
});
return loaded;
}
onLoad(event, promise) {
async onLoad(event, promise) {
this.window = this.iframe.contentWindow;
this.document = this.iframe.contentDocument;
this.contents = new Contents(this.document, this.document.body, this.section.cfiBase, this.section.index);
this.contents = new Contents(this.document, this.document.body, this.section.canonical, this.section.index);
this.rendering = false;
var link = this.document.querySelector("link[rel='canonical']");
if (link) {
link.setAttribute("href", this.section.canonical);
} else {
link = this.document.createElement("link");
link.setAttribute("rel", "canonical");
link.setAttribute("href", this.section.canonical);
this.document.querySelector("head").appendChild(link);
if (document.fonts) {
await document.fonts.ready;
}
this.contents.on(EVENTS.CONTENTS.EXPAND, () => {
@ -503,12 +499,12 @@ class IframeView {
//TODO: remove content listeners for expanding
}
display(request) {
display(requestMethod) {
var displayed = new defer();
if (!this.displayed) {
this.render(request)
this.render(requestMethod)
.then(function () {
this.emit(EVENTS.VIEWS.DISPLAYED, this);

View file

@ -1,4 +1,4 @@
import EventEmitter from "event-emitter";
import EventEmitter from "../../utils/eventemitter.js";
import {extend, borders, uuid, isNumber, bounds, defer, qs, parse} from "../../utils/core";
import EpubCFI from "../../epubcfi";
import Contents from "../../contents";

235
src/manifest/manifest.js Normal file
View file

@ -0,0 +1,235 @@
import Publication from "../publication/publication.js";
import { createUrl, resolve } from "../utils/url.js";
/**
* A representation of a Manifest with methods for the loading and manipulation
* of its contents.
* @class
* @param {json} [manifest]
* @returns {Manifest}
* @example new Manifest(manifest)
*/
class Manifest extends Publication {
constructor(url, options) {
super();
if (url) {
this.opened = this.open(url);
}
}
async unpack(json) {
for (const [key, value] of Object.entries(json)) {
switch (key) {
case "readingOrder":
this.readingOrder = value;
break;
case "resources":
this.resources = value;
break;
default:
this.setMetadata(key, value);
break;
}
}
const contentsUrl = this.contentsUrl;
if (contentsUrl) {
this.contents = await this.loadContents(contentsUrl);
}
}
async open(url) {
this.url = url;
const manifest = await this.load(url, "json");
return this.unpack(manifest);
}
async loadContents(contentsUrl) {
const html = await this.load(contentsUrl, "html");
let items = [];
let url = createUrl(contentsUrl);
const title = html.querySelector("h1, h2, h3, h4, h5, h6");
const toc = html.querySelector("*[role='doc-toc']");
if (toc) {
const links = toc.querySelectorAll("ol > li > a, ul > li > a");
for (const link of links) {
items.push(this.contentsEntry(link, url));
}
}
return items;
}
contentsEntry(link, contentsUrl) {
const href = link.getAttribute("href")
const url = resolve(contentsUrl, href);
let item = {
url: url,
name: link.textContent
}
const children = link.querySelectorAll("ol > li > a, ul > li > a");
if (children.length) {
item.children = [];
for (const child of children) {
item.children.push(this.contentsEntry(child, contentsUrl));
}
}
return item;
}
setMetadata(key, value) {
if (key === "readingProgression") {
this.metadata.set("direction", value);
}
if (key === "url") {
this.url = value;
}
this.metadata.set(key, value);
}
/**
* Get or set the cover url
* @param {string} [url]
* @return {string} coverUrl
*/
get coverUrl() {
let coverResource = this.resources.find((resource) => {
return resource.rel.includes("cover");
});
return coverResource && coverResource.url;
}
set coverUrl(url) {
let coverResource = this.resources.find((resource) => {
return resource.includes("cover");
});
if (coverResource) {
coverResource.url = url;
} else {
coverResource = new Resource({
rel: ["cover"],
url: url
});
this.resources.push(coverResource);
}
return coverResource && coverResource.url;
}
/**
* Get or set the table of contents url
* @param {string} [url]
* @return {string} contents
*/
get contentsUrl() {
let contentsUrl = this.resources.find((resource) => {
return resource.rel.includes("contents");
});
return contentsUrl && contentsUrl.url;
}
set contentsUrl(url) {
let contentsUrl = this.resources.find((resource) => {
return resource.rel.includes("contents");
});
if (contentsUrl) {
contentsUrl.url = url;
} else {
contentsUrl = {
rel: ["contents"],
url: url
};
this.resources.push(contentsUrl);
}
return contentsUrl && contentsUrl.url;
}
/**
* Get or set the landmarksUrl url
* @param {string} [url]
* @return {string} landmarksUrl
*/
get landmarksUrl() {
let landmarksUrl = this.resources.find((resource) => {
return resource.rel.includes("landmarks");
});
return landmarksUrl && landmarksUrl.url;
}
set landmarksUrl(url) {
let landmarksUrl = this.resources.find((resource) => {
return resource.rel.includes("landmarks");
});
if (landmarksUrl) {
landmarksUrl.url = url;
} else {
landmarksUrl = {
rel: ["landmarks"],
url: url
};
this.resources.push(landmarksUrl);
}
return landmarksUrl && landmarksUrl.url;
}
/**
* Get or set the pagelist url
* @param {string} [url]
* @return {string} pagelistUrl
*/
get pagelistUrl() {
let pagelistUrl = this.resources.find((resource) => {
return resource.rel.includes("pagelist");
});
return pagelistUrl && pagelistUrl.url;
}
set pagelistUrl(url) {
let pagelistUrl = this.resources.find((resource) => {
return resource.rel.includes("pagelist");
});
if (pagelistUrl) {
pagelistUrl.url = url;
} else {
pagelistUrl = {
rel: ["pagelist"],
url: url
};
this.resources.push(pagelistUrl);
}
return pagelistUrl && pagelistUrl.url;
}
get readingOrder() {
return this.sections;
}
set readingOrder(items) {
return this.sections = items;
}
get contents() {
return this.navigation;
}
set contents(items) {
return this.navigation = items;
}
}
export default Manifest;

124
src/marks/events.js Normal file
View file

@ -0,0 +1,124 @@
// import 'babelify/polyfill'; // needed for Object.assign
export default {
proxyMouse: proxyMouse
};
/**
* Start proxying all mouse events that occur on the target node to each node in
* a set of tracked nodes.
*
* The items in tracked do not strictly have to be DOM Nodes, but they do have
* to have dispatchEvent, getBoundingClientRect, and getClientRects methods.
*
* @param target {Node} The node on which to listen for mouse events.
* @param tracked {Node[]} A (possibly mutable) array of nodes to which to proxy
* events.
*/
export function proxyMouse(target, tracked) {
function dispatch(e) {
// We walk through the set of tracked elements in reverse order so that
// events are sent to those most recently added first.
//
// This is the least surprising behaviour as it simulates the way the
// browser would work if items added later were drawn "on top of"
// earlier ones.
for (var i = tracked.length - 1; i >= 0; i--) {
var t = tracked[i];
var x = e.clientX
var y = e.clientY;
if (e.touches && e.touches.length) {
x = e.touches[0].clientX;
y = e.touches[0].clientY;
}
if (!contains(t, target, x, y)) {
continue;
}
// The event targets this mark, so dispatch a cloned event:
t.dispatchEvent(clone(e));
// We only dispatch the cloned event to the first matching mark.
break;
}
}
if (target.nodeName === "iframe" || target.nodeName === "IFRAME") {
try {
// Try to get the contents if same domain
this.target = target.contentDocument;
} catch(err){
this.target = target;
}
} else {
this.target = target;
}
for (var ev of ['mouseup', 'mousedown', 'click', 'touchstart']) {
this.target.addEventListener(ev, (e) => dispatch(e), false);
}
}
/**
* Clone a mouse event object.
*
* @param e {MouseEvent} A mouse event object to clone.
* @returns {MouseEvent}
*/
export function clone(e) {
var opts = Object.assign({}, e, {bubbles: false});
try {
return new MouseEvent(e.type, opts);
} catch(err) { // compat: webkit
var copy = document.createEvent('MouseEvents');
copy.initMouseEvent(e.type, false, opts.cancelable, opts.view,
opts.detail, opts.screenX, opts.screenY,
opts.clientX, opts.clientY, opts.ctrlKey,
opts.altKey, opts.shiftKey, opts.metaKey,
opts.button, opts.relatedTarget);
return copy;
}
}
/**
* Check if the item contains the point denoted by the passed coordinates
* @param item {Object} An object with getBoundingClientRect and getClientRects
* methods.
* @param x {Number}
* @param y {Number}
* @returns {Boolean}
*/
function contains(item, target, x, y) {
// offset
var offset = target.getBoundingClientRect();
function rectContains(r, x, y) {
var top = r.top - offset.top;
var left = r.left - offset.left;
var bottom = top + r.height;
var right = left + r.width;
return (top <= y && left <= x && bottom > y && right > x);
}
// Check overall bounding box first
var rect = item.getBoundingClientRect();
if (!rectContains(rect, x, y)) {
return false;
}
// Then continue to check each child rect
var rects = item.getClientRects();
for (var i = 0, len = rects.length; i < len; i++) {
if (rectContains(rects[i], x, y)) {
return true;
}
}
return false;
}

236
src/marks/marks.js Normal file
View file

@ -0,0 +1,236 @@
// From https://github.com/nickstenning/marks
import svg from './svg.js';
import events from './events.js';
export class Pane {
constructor(target, container = document.body) {
this.target = target;
this.element = svg.createElement('svg');
this.marks = [];
// Match the coordinates of the target element
this.element.style.position = 'absolute';
// Disable pointer events
this.element.setAttribute('pointer-events', 'none');
// Set up mouse event proxying between the target element and the marks
events.proxyMouse(this.target, this.marks);
this.container = container;
this.container.appendChild(this.element);
this.render();
}
addMark(mark) {
var g = svg.createElement('g');
this.element.appendChild(g);
mark.bind(g, this.container);
this.marks.push(mark);
mark.render();
return mark;
}
removeMark(mark) {
var idx = this.marks.indexOf(mark);
if (idx === -1) {
return;
}
var el = mark.unbind();
this.element.removeChild(el);
this.marks.splice(idx, 1);
}
render() {
setCoords(this.element, coords(this.target, this.container));
for (var m of this.marks) {
m.render();
}
}
}
export class Mark {
constructor() {
this.element = null;
}
bind(element, container) {
this.element = element;
this.container = container;
}
unbind() {
var el = this.element;
this.element = null;
return el;
}
render() {}
dispatchEvent(e) {
if (!this.element) return;
this.element.dispatchEvent(e);
}
getBoundingClientRect() {
return this.element.getBoundingClientRect();
}
getClientRects() {
var rects = [];
var el = this.element.firstChild;
while (el) {
rects.push(el.getBoundingClientRect());
el = el.nextSibling;
}
return rects;
}
filteredRanges() {
if (!this.range) {
return [];
}
// De-duplicate the boxes
const rects = Array.from(this.range.getClientRects());
const stringRects = rects.map((r) => JSON.stringify(r));
const setRects = new Set(stringRects);
return Array.from(setRects).map((sr) => JSON.parse(sr));
}
}
export class Highlight extends Mark {
constructor(range, className, data, attributes) {
super();
this.range = range;
this.className = className;
this.data = data || {};
this.attributes = attributes || {};
}
bind(element, container) {
super.bind(element, container);
for (var attr in this.data) {
if (this.data.hasOwnProperty(attr)) {
this.element.dataset[attr] = this.data[attr];
}
}
for (var attr in this.attributes) {
if (this.attributes.hasOwnProperty(attr)) {
this.element.setAttribute(attr, this.attributes[attr]);
}
}
if (this.className) {
this.element.classList.add(this.className);
}
}
render() {
// Empty element
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
var docFrag = this.element.ownerDocument.createDocumentFragment();
var filtered = this.filteredRanges();
var offset = this.element.getBoundingClientRect();
var container = this.container.getBoundingClientRect();
for (var i = 0, len = filtered.length; i < len; i++) {
var r = filtered[i];
var el = svg.createElement('rect');
el.setAttribute('x', r.left - offset.left + container.left);
el.setAttribute('y', r.top - offset.top + container.top);
el.setAttribute('height', r.height);
el.setAttribute('width', r.width);
docFrag.appendChild(el);
}
this.element.appendChild(docFrag);
}
}
export class Underline extends Highlight {
constructor(range, className, data, attributes) {
super(range, className, data, attributes);
}
render() {
// Empty element
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
var docFrag = this.element.ownerDocument.createDocumentFragment();
var filtered = this.filteredRanges();
var offset = this.element.getBoundingClientRect();
var container = this.container.getBoundingClientRect();
for (var i = 0, len = filtered.length; i < len; i++) {
var r = filtered[i];
var rect = svg.createElement('rect');
rect.setAttribute('x', r.left - offset.left + container.left);
rect.setAttribute('y', r.top - offset.top + container.top);
rect.setAttribute('height', r.height);
rect.setAttribute('width', r.width);
rect.setAttribute('fill', 'none');
var line = svg.createElement('line');
line.setAttribute('x1', r.left - offset.left + container.left);
line.setAttribute('x2', r.left - offset.left + container.left + r.width);
line.setAttribute('y1', r.top - offset.top + container.top + r.height - 1);
line.setAttribute('y2', r.top - offset.top + container.top + r.height - 1);
line.setAttribute('stroke-width', 1);
line.setAttribute('stroke', 'black'); //TODO: match text color?
line.setAttribute('stroke-linecap', 'square');
docFrag.appendChild(rect);
docFrag.appendChild(line);
}
this.element.appendChild(docFrag);
}
}
function coords(el, container) {
var offset = container.getBoundingClientRect();
var rect = el.getBoundingClientRect();
return {
top: rect.top - offset.top,
left: rect.left - offset.left,
height: el.scrollHeight,
width: el.scrollWidth
};
}
function setCoords(el, coords) {
el.style.setProperty('top', `${coords.top}px`, 'important');
el.style.setProperty('left', `${coords.left}px`, 'important');
el.style.setProperty('height', `${coords.height}px`, 'important');
el.style.setProperty('width', `${coords.width}px`, 'important');
}
function contains(rect1, rect2) {
return (
(rect2.right <= rect1.right) &&
(rect2.left >= rect1.left) &&
(rect2.top >= rect1.top) &&
(rect2.bottom <= rect1.bottom)
);
}

7
src/marks/svg.js Normal file
View file

@ -0,0 +1,7 @@
export function createElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
export default {
createElement: createElement
};

View file

@ -0,0 +1,92 @@
import ResourceList from "./resourcelist.js";
/**
*
*/
class Locator {
constructor(item = {}) {
this.data = {
url: item.url,
index: item.index,
id: item.id,
cfi: item.cfi,
type: item.type,
name: item.name,
parent: undefined,
children: new ResourceList()
}
}
get url() {
return this.data.url;
}
set url(url) {
this.data.url = url;
}
get index() {
return this.data.index;
}
set index(index) {
this.data.index = index;
}
get id() {
return this.data.id;
}
set id(id) {
this.data.id = id;
}
get type() {
return this.data.type;
}
set type(type) {
this.data.type = type;
}
get name() {
return this.data.name;
}
set name(name) {
return this.data.name;
}
get parent() {
return this.data.parent;
}
set parent(item) {
this.data.parent = item;
return this.data.parent;
}
get children() {
return this.data.children;
}
set children(items) {
for (const childItem of items) {
const child = new Locator(childItem);
child.parent = this;
this.children.append(child);
}
return this.data.children;
}
toJSON() {
return JSON.stringify(this);
}
destroy() {
}
}
export default Locator;

233
src/publication/locators.js Normal file
View file

@ -0,0 +1,233 @@
import {qs, sprint, locationOf, defer} from "../utils/core.js";
import Queue from "../utils/queue.js";
import EpubCFI from "../utils/epubcfi.js";
import { EVENTS } from "../utils/constants.js";
import EventEmitter from "../utils/eventemitter.js";
/**
* Locators
* @param {object} [manifest]
*/
class Locators {
constructor(manifest) {
if (manifest) {
this.unpack(manifest);
}
}
unpack(manifest) {
if (manifest.locations) {
this.unpackLocations(manifest.locations);
}
if (manifest.pages) {
this.unpackPages(manifest.page);
}
}
unpackLocations(locations) {
this.locations = locations;
this.totalLocations = this.locations.length - 1;
}
unpackPages(pages) {
this.pages = pages;
this.firstPage = parseInt(this.pages[0]);
this.lastPage = parseInt(this.pages[this.pages.length-1]);
this.totalPages = this.lastPage - this.firstPage;
pages.forEach((item) => {
if (item.cfi) {
this.pageLocations.push(item.cfi);
}
});
}
/**
* Get a location from an EpubCFI
* @param {EpubCFI} cfi
* @return {number}
*/
locationFromCfi(cfi){
let loc;
if (EpubCFI.prototype.isCfiString(cfi)) {
cfi = new EpubCFI(cfi);
}
// Check if the location has not been set yet
if(this.locations.length === 0) {
return -1;
}
loc = locationOf(cfi, this.locations, EpubCFI.prototype.compare);
if (loc > this.totalLocations) {
return this.totalLocations;
}
return loc;
}
/**
* Get a percentage position in locations from an EpubCFI
* @param {EpubCFI} cfi
* @return {number}
*/
percentageFromCfi(cfi) {
if(this.locations.length === 0) {
return null;
}
// Find closest cfi
var loc = this.locationFromCfi(cfi);
// Get percentage in total
return this.percentageFromLocation(loc);
}
/**
* Get a percentage position from a location index
* @param {number} location
* @return {number}
*/
percentageFromLocation(loc) {
if (!loc || !this.totalLocations) {
return 0;
}
return (loc / this.totalLocations);
}
/**
* Get an EpubCFI from location index
* @param {number} loc
* @return {EpubCFI} cfi
*/
cfiFromLocation(loc){
var cfi = -1;
// check that pg is an int
if(typeof loc != "number"){
loc = parseInt(loc);
}
if(loc >= 0 && loc < this.locations.length) {
cfi = this.locations[loc];
}
return cfi;
}
/**
* Get an EpubCFI from location percentage
* @param {number} percentage
* @return {EpubCFI} cfi
*/
cfiFromPercentage(percentage){
let loc;
if (percentage > 1) {
console.warn("Normalize cfiFromPercentage value to between 0 - 1");
}
// Make sure 1 goes to very end
if (percentage >= 1) {
let cfi = new EpubCFI(this.locations[this.totalLocations]);
cfi.collapse();
return cfi.toString();
}
loc = Math.ceil(this.totalLocations * percentage);
return this.cfiFromLocation(loc);
}
/**
* Get a PageList result from a EpubCFI
* @param {string} cfi EpubCFI String
* @return {string} page
*/
pageFromCfi(cfi){
var pg = -1;
// Check if the pageList has not been set yet
if(!this.pageLocations || this.pageLocations.length === 0) {
return -1;
}
// check if the cfi is in the location list
var index = indexOfSorted(cfi, this.pageLocations, EpubCFI.prototype.compare);
if(index != -1) {
pg = this.pages[index];
} else {
// Otherwise add it to the list of locations
// Insert it in the correct position in the locations page
index = locationOf(cfi, this.pageLocations, EpubCFI.prototype.compare);
// Get the page at the location just before the new one, or return the first
pg = index-1 >= 0 ? this.pages[index-1] : this.pages[0];
if(pg !== undefined) {
// Add the new page in so that the locations and page array match up
//this.pages.splice(index, 0, pg);
} else {
pg = -1;
}
}
return pg;
}
/**
* Get an EpubCFI from a Page List Item
* @param {string} pg
* @return {string} cfi
*/
cfiFromPage(pg){
var cfi = -1;
// check that pg is an int
if(typeof pg != "number"){
pg = parseInt(pg);
}
// check if the cfi is in the page list
// Pages could be unsorted.
var index = this.pages.indexOf(pg);
if(index != -1) {
cfi = this.pageLocations[index];
}
// TODO: handle pages not in the list
return cfi;
}
/**
* Get a Page from Book percentage
* @param {number} percent
* @return {string} page
*/
pageFromPercentage(percent){
var pg = Math.round(this.totalPages * percent);
return pg;
}
/**
* Returns a value between 0 - 1 corresponding to the location of a page
* @param {int} pg the page
* @return {number} percentage
*/
percentageFromPage(pg){
var percentage = (pg - this.firstPage) / this.totalPages;
return Math.round(percentage * 1000) / 1000;
}
/**
* Returns a value between 0 - 1 corresponding to the location of a cfi
* @param {string} cfi EpubCFI String
* @return {number} percentage
*/
percentagePageFromCfi(cfi){
var pg = this.pageFromCfi(cfi);
var percentage = this.percentageFromPage(pg);
return percentage;
}
destroy () {
}
}
EventEmitter(Locators.prototype);
export default Locators;

View file

@ -0,0 +1,107 @@
import {
qs,
qsa,
querySelectorByType,
filterChildren,
getParentByTagName,
uuid
} from "../utils/core.js";
/**
* Navigation wrapper
* @param {[object]} manifest
*/
class Navigation {
constructor(manifest) {
this.toc = [];
this.tocByHref = {};
this.tocById = {};
this.landmarks = [];
this.landmarksByType = {};
if (manifest) {
this.unpack(manifest);
}
}
/**
* Get an item from the navigation
* @param {string} target
* @return {object} navItems
*/
get(target) {
var index;
if(!target) {
return this.toc[0];
}
if(target.indexOf("#") === 0) {
index = this.tocById[target.substring(1)];
} else if(target in this.tocByHref){
index = this.tocByHref[target];
}
return this.toc[index];
}
/**
* Get a landmark by type
* List of types: https://idpf.github.io/epub-vocabs/structure/
* @param {string} type
* @return {object} landmarkItems
*/
landmark(type) {
let index;
index = this.landmarksByType[type];
return this.landmarks[index];
}
/**
* Unpack manifest object
*/
unpack(manifest) {
if (manifest.toc) {
this.unpackToc(manifest.toc);
}
if (manifest.landmarks) {
this.unpackLandmarks(manifest.landmarks);
}
}
unpackToc(toc) {
this.toc = toc;
toc.forEach((item, index) => {
this.tocByHref[item.href] = index;
if (item.source) {
this.tocByHref[item.href] = index;
}
if (item.id) {
this.tocId[item.id] = index;
}
});
}
unpackLandmarks(landmarks) {
this.landmarks = landmarks;
landmarks.forEach((item, index) => {
this.landmarksByType[item.type] = index;
});
}
destroy() {
this.toc = undefined;
this.tocByHref = undefined;
this.tocById = undefined;
this.landmarks = undefined;
this.landmarksByType = undefined;
}
}
export default Navigation;

View file

@ -0,0 +1,431 @@
import EventEmitter from "../utils/eventemitter.js";
import { resolve, isAbsolute, filename } from "../utils/url.js";
import ResourceList from "./resourcelist.js";
import request from "../utils/request.js";
import EpubCFI from "../utils/epubcfi.js";
import { EPUBJS_VERSION } from "../utils/constants.js";
import Resource from "./resource.js";
import Locator from "./locator.js";
import DisplayOptions from "../epub/displayoptions.js";
/**
* A representation of a Publication with methods for the loading and manipulation
* of its contents.
* @class
* @param {json | object} [manifest]
* @returns {Publication}
* @example new Publication(manifest)
*/
class Publication {
constructor(data, requestMethod, requestOptions) {
/**
* @member {object} data
* @memberof Publication
* @private
*/
this.data = {
url: "",
metadata: new Map(),
navigation: new ResourceList(),
landmarks: new ResourceList(),
locations: new ResourceList(),
pagelist: new ResourceList(),
sections: new ResourceList(),
resources: new ResourceList(),
links: new ResourceList(),
uniqueResources: new ResourceList(),
displayOptions: new DisplayOptions()
};
if (requestMethod) {
this.request = requestMethod;
}
if (requestOptions) {
this.requestOptions = requestOptions;
}
if (data) {
this.opened = this.open(data);
} else {
this.opened = Promise.resolve();
}
}
async open() {
this.parse(data);
}
async unpack(data) {
if (!data) {
return;
}
if (typeof data === "string") {
data = JSON.parse(data);
}
let {
url,
metadata,
resources,
toc,
landmarks,
locations,
pagelist,
sections,
links
} = data;
this.url = url;
this.metadata = metadata;
this.resources = resources;
this.sections = sections;
this.toc = toc;
this.landmarks = landmarks;
this.locations = locations;
this.pagelist = pagelist;
this.links = links;
}
/**
* Load a resource from the Book
* @private
* @param {string} path path to the resource to load
* @return {Promise} returns a promise with the requested resource
*/
load(path, type) {
const resolved = resolve(this.url, path);
return this.request ? this.request(resolved, type, this.requestOptions) : request(resolved, type, this.requestOptions);
}
/**
* Resolve a path to it's absolute position in the Publication
* @private
* @param {string} path
* @param {boolean} [absolute] force resolving the full URL
* @return {string} the resolved path string
*/
resolve(path, absolute) {
if (!path) {
return;
}
let resolved = path;
if (isAbsolute(path)) {
return path;
}
if (this.url) {
resolved = resolve(this.url, path);
}
return resolved;
}
/**
* Get or set the Url
* @param {string} [url]
* @return {string} href
*/
get url() {
return this.data.url;
}
set url(url) {
this.data.url = url;
return this.data.url;
}
/**
* Get or set the readingOrder
* @param {array} [spineItems]
* @return {array} spineItems
*/
get sections() {
return this.data.sections;
}
set sections(items) {
if (!items) {
return;
}
let index = 1;
for (const item of items) {
item.url = resolve(this.url, item.url);
// TEMP hack for handling EpubCFI
const id = encodeURIComponent(filename(item.url).split(".")[0]);
item.id = id;
// Index 2 for Sections
item.canonical = item.canonical || `2/${index * 2}[${id}]`;
const resource = new Resource(item);
this.data.sections.append(resource);
this.data.uniqueResources.add(resource);
index += 1;
}
return this.data.sections;
}
/**
* Get or set the metadata
* @param {object} [metadata]
* @return {object} metadata
*/
get metadata() {
return this.data.metadata;
}
set metadata(metadata) {
if (!metadata) {
return;
}
for (const [key, value] of Object.entries(metadata)) {
this.data.metadata.set(key, value);
}
return this.data.metadata;
}
/**
* Get or set the resources
* @param {object} [resources]
* @return {object} resources
*/
get resources() {
return this.data.resources;
}
set resources(items) {
if (!items) {
return;
}
let index = 1;
for (const item of items) {
item.url = this.resolve(item.url);
// TEMP hack for handling EpubCFI
const id = encodeURIComponent(filename(item.url).split(".")[0]);
item.id = id;
// Index 4 for Resources
item.canonical = item.canonical || `4/${index * 2}[${id}]`;
const resource = new Resource(item);
this.data.resources.add(resource);
this.data.uniqueResources.add(resource);
index += 1;
}
return this.data.resources;
}
/**
* Get or set the uniqueResources
* @param {object} [resources]
* @return {object} resources
*/
get uniqueResources() {
return this.data.uniqueResources;
}
set uniqueResources(items) {
if (!items) {
return;
}
for (const item of items) {
item.url = this.resolve(item.url);
item.canonical = item.canonical || item.url;
const resource = new Resource(item);
this.data.uniqueResources.add(resource);
}
return this.data.uniqueResources;
}
/**
* Get or set the toc
* @param {array} [toc]
* @return {array} toc
*/
get navigation() {
return this.data.navigation;
}
set navigation(items) {
if (!items) {
return;
}
for (const item of items) {
item.url = this.resolve(item.url);
item.canonical = item.canonical || item.url;
const loc = new Locator(item);
this.data.navigation.append(loc);
}
return this.data.navigation;
}
/**
* Get or set the landmarks
* @param {array} [landmarks]
* @return {array} landmarks
*/
get landmarks() {
return this.data.landmarks;
}
set landmarks(items) {
if (!items) {
return;
}
for (const item of items) {
item.url = this.resolve(item.url);
item.canonical = item.canonical || item.url;
const loc = new Locator(item);
this.data.landmarks.append(loc);
}
return this.data.landmarks;
}
/**
* Get or set the locations
* @param {array} [locations]
* @return {array} locations
*/
get locations() {
return this.data.locations;
}
set locations(items) {
if (!items) {
return;
}
for (const item of items) {
item.url = this.resolve(item.url);
item.canonical = item.canonical || item.url;
const loc = new Locator(item);
this.data.locations.append(loc);
}
return this.data.locations;
}
/**
* Get or set the pagelist
* @param {array} [pageList]
* @return {array} pageList
*/
get pagelist() {
return this.data.pagelist;
}
set pagelist(items) {
if (!items) {
return;
}
for (const item of items) {
item.url = this.resolve(item.url);
item.canonical = item.canonical || item.url;
const loc = new Locator(item);
this.data.pagelist.append(loc);
}
return this.data.pagelist;
}
/**
* Get or set links
* @param {array} [links]
* @return {array} links
*/
get links() {
return this.data.links;
}
set links(links) {
return this.data.links = links;
}
get displayOptions() {
return this.data.displayOptions;
}
set displayOptions(options) {
this.data.displayOptions = new DisplayOptions(options);
return this.data.displayOptions;
}
/**
* Find a DOM Range for a given CFI Range
* @param {EpubCFI} cfiRange a epub cfi range
* @return {Range}
*/
getRange(cfiRange) {
var cfi = new EpubCFI(cfiRange);
var item = this.sections.get(cfi.spinePos);
if (!item) {
return new Promise((resolve, reject) => {
reject("CFI could not be found");
});
}
return item.load().then(function (contents) {
var range = cfi.toRange(item.document);
return range;
});
}
/**
* Generates the Publication Key using the identifer in the data or other string provided
* @param {string} [identifier] to use instead of metadata identifier
* @return {string} key
*/
key(identifier) {
var ident = identifier || this.metadata.identifier;
return `epubjs-${EPUBJS_VERSION}-${ident}`;
}
/**
* Generates a object representation of the publication structure
* @return {object}
*/
toObject() {
return this.data;
}
/**
* Generates a JSON output of the publication structure
*/
toJSON() {
return JSON.stringify(this.data);
}
/**
* Destroy the Publication and all associated objects
*/
destroy() {
this.data = undefined;
}
}
//-- Enable binding events to publication
EventEmitter(Publication.prototype);
export default Publication;

126
src/publication/resource.js Normal file
View file

@ -0,0 +1,126 @@
import request from "../utils/request.js";
import { lookup } from "../utils/mime.js";
/**
*
*/
class Resource {
constructor(item = {}, loader) {
if (item && !item.url) {
throw new Error("Resource url is required.");
}
if (item.rel || !Array.isArray(item.rel)) {
item.rel = [item.rel];
}
this.data = {
url: item.url,
index: item.index,
id: item.id,
canonical: item.canonical,
type: item.type,
encoding: item.encoding || lookup(item.url),
properites: item.properites,
rel: item.rel || [],
name: item.name,
cfiPos: item.cfiPos
}
}
get url() {
return this.data.url;
}
set url(url) {
this.data.url = url;
}
get index() {
return this.data.index;
}
set index(index) {
this.data.index = index;
}
get id() {
return this.data.id;
}
set id(id) {
this.data.id = id;
}
get canonical() {
return this.data.canonical;
}
set canonical(canonical) {
this.data.canonical = canonical;
}
get type() {
return this.data.type;
}
set type(type) {
this.data.type = type;
}
get encoding() {
return this.data.encoding;
}
set encoding(encoding) {
this.data.id = encoding;
}
get properites() {
return this.data.properites;
}
set properites(properites) {
this.data.id = properites;
}
get rel() {
return this.data.rel;
}
set rel(rel) {
this.data.rel = rel;
}
get name() {
return this.data.name;
}
set name(name) {
this.data.name = name;
}
get cfiPos() {
return this.data.cfiPos;
}
set cfiPos(pos) {
this.data.cfiPos = pos;
}
/**
* Load the resource from its url
*/
async load() {
return request(this.url, this.type)
}
toJSON() {
}
destroy() {
}
}
export default Resource;

View file

@ -0,0 +1,195 @@
import { isNumber } from "../utils/core.js";
/**
* A collection of Resources
*/
class ResourceList extends Map {
constructor(items) {
super(items)
this.order = Array.from(this.entries());
this.ids = new Map();
}
// unordered
add(resource) {
const { url } = resource;
this.set(url, resource);
if (resource.id) {
this.ids.set(resource.id, url);
}
return this.size;
}
// ordered
append(resource) {
const { url } = resource;
const index = this.size;
resource.index = index;
this.order.push(url);
this.set(url, resource);
if (resource.id) {
this.ids.set(resource.id, url);
}
return index;
}
prepend(resource) {
const { url } = resource;
const index = 0;
resource.index = index;
this.order.unshift(url);
if (resource.id) {
this.ids.set(resource.id, url);
}
const tempMap = new Map(this);
this.clear()
this.set(url, resource);
// Re-index
let newIndex = 1;
tempMap.forEach((value, key) => {
value.index = newIndex;
this.set(key, value);
newIndex++;
});
return index;
}
insert(resource, index) {
const { url } = resource;
resource.index = index;
if (resource.id) {
this.ids.set(resource.id, url);
}
if (index > -1) {
this.order.splice(index, 1);
const tempMap = new Map(this);
this.clear()
let newIndex = 0;
tempMap.forEach((value, key) => {
if (index === newIndex) {
this.set(url, resource);
newIndex++;
}
if (index < newIndex) {
value.index = newIndex;
this.set(key, value);
}
newIndex++;
});
}
}
delete(resource) {
const { url } = resource;
super.delete(resource);
if (resource.id) {
this.ids.delete(resource.id);
}
const index = this.order.indexOf(url);
if (index > -1) {
this.order.splice(index, 1);
let newIndex = 0;
this.forEach((value, key) => {
value.index = newIndex;
newIndex++;
});
}
}
get(what) {
if (isNumber(what)) {
const url = (what > -1) && (what < this.order.length) && this.order[what];
if (url) {
return this.get(url);
}
}
return super.get(what);
}
id(what) {
let index = this.ids.get(what);
if (index) {
return this.get(index);
}
}
first() {
const url = this.order.length && this.order[0];
if (url) {
return this.get(url);
}
}
last() {
const url = this.order.length && this.order[this.order.length - 1];
if (url) {
return this.get(url);
}
}
find(fn) {
let result;
if (!fn) {
return;
}
for (const [key, resource] of this) {
let result = fn(resource);
if (result) {
return resource;
}
}
}
map(fn) {
let results = [];
if (!fn) {
return;
}
for (const [key, resource] of this) {
let result = fn(resource);
if (result) {
results.push(result);
}
}
return results;
}
get length() {
return this.size;
}
toArray() {
return Array.from(this.values());
}
toJSON() {
return JSON.stringify(this.toArray());
}
}
export default ResourceList;

View file

@ -0,0 +1,160 @@
/**
* Handles Package Resources
* @class
* @param {object} resources
* @param {object} [options]
* @param {string} [options.replacements="base64"]
* @param {Archive} [options.archive]
* @param {method} [options.load]
* @param {string} [options.url]
* @param {string} [options.inject]
*/
class Resources {
constructor(resources) {
this.urlCache = {};
this.resources = Object.assign({}, resources);
this.resourcesByHref = {};
this.ids = [];
this.html = [];
this.assets = [];
this.css = [];
if (resources) {
this.split(resources);
}
}
/**
* Split resources by type
* @private
*/
split(resources){
let keys = Object.keys(resources);
// HTML
let html = keys.
filter(function (key){
let item = resources[key];
if (item.type === "application/xhtml+xml" ||
item.type === "text/html") {
return true;
}
});
// Exclude HTML & CSS
let assets = keys.
filter(function (key){
let item = resources[key];
if (item.type !== "application/xhtml+xml" &&
item.type !== "text/html" &&
item.type !== "text/css") {
return true;
}
});
// Only CSS
let css = keys.
filter(function (key){
let item = resources[key];
if (item.type === "text/css") {
return true;
}
});
keys.forEach((id) => {
let resource = resources[id];
// set ID from keys
resource.id = id;
if (!resource.source) {
resource.source = resource.href;
}
this.resourcesByHref[resource.href] = id;
});
this.ids = keys;
this.html = html;
this.assets = assets;
this.css = css;
return {
html,
assets,
css
}
}
/**
* Export an Array of all resources
* @return {array}
*/
toArray() {
return this.ids.map((key) => {
let item = this.resources[key];
let { type, properties, id } = item;
let source = item.href;
let href = item.href;
return {
href,
source,
type,
properties,
id
};
});
}
forEach(func) {
return this.ids.
forEach((id) => {
let r = this.resources[id];
r.id = key;
func(r);
});
}
map(func) {
return this.ids.
map((id) => {
let r = this.resources[id];
r.id = key;
return func(r);
});
}
filter(func) {
return this.ids.
filter((id) => {
let r = this.resources[id];
r.id = key;
return func(r);
});
}
get(what) {
if (what in this.resources) {
return this.resources[what];
} else if (what in this.resourcesByHref) {
let id = this.resourcesByHref[what];
return this.resources[id];
}
}
destroy() {
this.settings = undefined;
this.manifest = undefined;
this.html = undefined;
this.assets = undefined;
this.css = undefined;
this.urls = undefined;
this.cssUrls = undefined;
}
}
export default Resources;

56
src/publication/search.js Normal file
View file

@ -0,0 +1,56 @@
/**
* Find a string in a section
* @param {string} _query The query string to find
* @return {object[]} A list of matches, with form {cfi, excerpt}
*/
find(_query){
var section = this;
var matches = [];
var query = _query.toLowerCase();
var find = function (node) {
var text = node.textContent.toLowerCase();
var range = section.document.createRange();
var cfi;
var pos;
var last = -1;
var excerpt;
var limit = 150;
while (pos != -1) {
// Search for the query
pos = text.indexOf(query, last + 1);
if (pos != -1) {
// We found it! Generate a CFI
range = section.document.createRange();
range.setStart(node, pos);
range.setEnd(node, pos + query.length);
cfi = section.cfiFromRange(range);
// Generate the excerpt
if (node.textContent.length < limit) {
excerpt = node.textContent;
}
else {
excerpt = node.textContent.substring(pos - limit / 2, pos + limit / 2);
excerpt = "..." + excerpt + "...";
}
// Add the CFI to the matches list
matches.push({
cfi: cfi,
excerpt: excerpt
});
}
last = pos;
}
};
sprint(section.document, function (node) {
find(node);
});
return matches;
}

View file

@ -1,6 +1,6 @@
import EventEmitter from "event-emitter";
import EpubCFI from "./epubcfi";
import { EVENTS } from "./utils/constants";
import EventEmitter from "../utils/eventemitter.js";
import EpubCFI from "../utils/epubcfi.js";
import { EVENTS } from "../utils/constants.js";
/**
* Handles managing adding & removing Annotations

View file

@ -1,9 +1,9 @@
import EventEmitter from "event-emitter";
import {isNumber, prefixed, borders, defaults} from "./utils/core";
import EpubCFI from "./epubcfi";
import Mapping from "./mapping";
import {replaceLinks} from "./utils/replacements";
import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "./utils/constants";
import EventEmitter from "../utils/eventemitter.js";
import {isNumber, prefixed, borders, defaults} from "../utils/core.js";
import EpubCFI from "../utils/epubcfi.js";
import Mapping from "./mapping.js";
import {replaceLinks} from "../utils/replacements.js";
import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "../utils/constants.js";
const hasNavigator = typeof (navigator) !== "undefined";

View file

@ -1,6 +1,6 @@
import { extend } from "./utils/core";
import { EVENTS } from "./utils/constants";
import EventEmitter from "event-emitter";
import { extend } from "../utils/core.js";
import { EVENTS } from "../utils/constants.js";
import EventEmitter from "../utils/eventemitter.js";
/**
* Figures out the CSS values to apply for a layout

View file

@ -1,5 +1,5 @@
import EpubCFI from "./epubcfi";
import { nodeBounds } from "./utils/core";
import EpubCFI from "../utils/epubcfi.js";
import { nodeBounds } from "../utils/core.js";
/**
* Map text locations to CFI ranges
@ -22,7 +22,7 @@ class Mapping {
*/
section(view) {
var ranges = this.findRanges(view);
var map = this.rangeListToCfiList(view.section.cfiBase, ranges);
var map = this.rangeListToCfiList(view.section.canonical, ranges);
return map;
}

View file

@ -1,28 +1,31 @@
import EventEmitter from "event-emitter";
import { extend, defer, isFloat } from "./utils/core";
import Hook from "./utils/hook";
import EpubCFI from "./epubcfi";
import Queue from "./utils/queue";
import Layout from "./layout";
import EventEmitter from "../utils/eventemitter.js";
import { extend, defer, isFloat, isNumber, isXMLDocument } from "../utils/core.js";
import Hook from "../utils/hook.js";
import EpubCFI from "../utils/epubcfi.js";
import Queue from "../utils/queue.js";
import Layout from "./layout.js";
// import Mapping from "./mapping";
import Themes from "./themes";
import Contents from "./contents";
import Annotations from "./annotations";
import { EVENTS, DOM_EVENTS } from "./utils/constants";
import Themes from "./themes.js";
import Contents from "./contents.js";
import Annotations from "./annotations.js";
import Section from "./section.js";
import { EVENTS, DOM_EVENTS, XML_NS } from "../utils/constants.js";
// Default Views
import IframeView from "./managers/views/iframe";
import IframeView from "../managers/views/iframe.js";
// Default View Managers
import DefaultViewManager from "./managers/default/index";
import ContinuousViewManager from "./managers/continuous/index";
import DefaultViewManager from "../managers/default/default.js";
import ContinuousViewManager from "../managers/continuous/continuous.js";
import Publication from "../publication/publication.js";
/**
* Displays an Epub as a series of Views for each Section.
* Requires Manager and View class to handle specifics of rendering
* the section content.
* @class
* @param {Book} book
* @param {Publication} publication
* @param {object} [options]
* @param {number} [options.width]
* @param {number} [options.height]
@ -38,9 +41,10 @@ import ContinuousViewManager from "./managers/continuous/index";
* @param {boolean | object} [options.snap=false] use snap scrolling
* @param {string} [options.defaultDirection='ltr'] default text direction
* @param {boolean} [options.allowScriptedContent=false] enable running scripts in content
* @param {string | Element} [options.element] element to render to
*/
class Rendition {
constructor(book, options) {
constructor(publication, options) {
this.settings = extend(this.settings || {}, {
width: null,
@ -57,7 +61,9 @@ class Rendition {
script: null,
snap: false,
defaultDirection: "ltr",
allowScriptedContent: false
allowScriptedContent: false,
request: null,
canonical: null
});
extend(this.settings, options);
@ -66,7 +72,11 @@ class Rendition {
this.manager = this.settings.manager;
}
this.book = book;
this.publication = publication || new Publication();
if (!this.settings.request && typeof this.publication.request !== "undefined") {
this.settings.request = this.publication.request.bind(this.publication)
}
/**
* Adds Hook methods to the Rendition prototype
@ -76,6 +86,7 @@ class Rendition {
*/
this.hooks = {};
this.hooks.display = new Hook(this);
this.hooks.document = new Hook(this);
this.hooks.serialize = new Hook(this);
this.hooks.content = new Hook(this);
this.hooks.unloaded = new Hook(this);
@ -86,15 +97,17 @@ class Rendition {
this.hooks.content.register(this.handleLinks.bind(this));
this.hooks.content.register(this.passEvents.bind(this));
this.hooks.content.register(this.adjustImages.bind(this));
this.hooks.content.register(this.preloadImages.bind(this));
this.book.spine.hooks.content.register(this.injectIdentifier.bind(this));
this.hooks.document.register(this.injectIdentifier.bind(this));
this.hooks.document.register(this.injectBase.bind(this));
this.hooks.document.register(this.injectCanonical.bind(this));
if (this.settings.stylesheet) {
this.book.spine.hooks.content.register(this.injectStylesheet.bind(this));
this.hooks.document.register(this.injectStylesheet.bind(this));
}
if (this.settings.script) {
this.book.spine.hooks.content.register(this.injectScript.bind(this));
this.hooks.document.register(this.injectScript.bind(this));
}
/**
@ -119,7 +132,7 @@ class Rendition {
* @type {Object}
* @property {object} start
* @property {string} start.index
* @property {string} start.href
* @property {string} start.url
* @property {object} start.displayed
* @property {EpubCFI} start.cfi
* @property {number} start.location
@ -128,7 +141,7 @@ class Rendition {
* @property {number} start.displayed.total
* @property {object} end
* @property {string} end.index
* @property {string} end.href
* @property {string} end.url
* @property {object} end.displayed
* @property {EpubCFI} end.cfi
* @property {number} end.location
@ -141,18 +154,23 @@ class Rendition {
*/
this.location = undefined;
// Hold queue until book is opened
this.q.enqueue(this.book.opened);
// Hold queue until publication is opened
this.q.enqueue(this.publication.opened);
this.starting = new defer();
/**
* @member {promise} started returns after the rendition has started
* @memberof Rendition
*/
this.started = this.starting.promise;
// this.starting = new defer();
// /**
// * @member {promise} started returns after the rendition has started
// * @memberof Rendition
// */
// this.started = this.starting.promise;
// Block the queue until rendering is started
this.q.enqueue(this.start);
// Start rendering
if (this.settings.element) {
this.renderTo(this.settings.element);
}
}
/**
@ -208,10 +226,11 @@ class Rendition {
* @return {Promise} rendering has started
*/
start(){
if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) {
// TODO: Move this Fred
if (!this.settings.layout && (this.publication.metadata.layout === "pre-paginated" || this.publication.displayOptions.fixedLayout === "true")) {
this.settings.layout = "pre-paginated";
}
switch(this.book.package.metadata.spread) {
switch(this.publication.metadata.spread) {
case 'none':
this.settings.spread = 'none';
break;
@ -227,15 +246,17 @@ class Rendition {
this.manager = new this.ViewManager({
view: this.View,
queue: this.q,
request: this.book.load.bind(this.book),
hooks: this.hooks,
request: this.settings.request,
sections: this.publication.sections,
settings: this.settings
});
}
this.direction(this.book.package.metadata.direction || this.settings.defaultDirection);
this.direction(this.publication.metadata.direction || this.settings.defaultDirection);
// Parse metadata to get layout props
this.settings.globalLayoutProperties = this.determineLayoutProperties(this.book.package.metadata);
this.settings.globalLayoutProperties = this.determineLayoutProperties(this.publication.metadata);
this.flow(this.settings.globalLayoutProperties.flow);
@ -260,9 +281,6 @@ class Rendition {
* @memberof Rendition
*/
this.emit(EVENTS.RENDITION.STARTED);
// Start processing queue
this.starting.resolve();
}
/**
@ -271,10 +289,8 @@ class Rendition {
* @param {element} element to attach to
* @return {Promise}
*/
attachTo(element){
renderTo(element){
return this.q.enqueue(function () {
// Start rendering
this.manager.render(element, {
"width" : this.settings.width,
@ -289,13 +305,22 @@ class Rendition {
this.emit(EVENTS.RENDITION.ATTACHED);
}.bind(this));
}
/**
* Display a point in the book
* Alias for renderTo
* @alias renderTo
* @param {element} element to attach to
* @return {Promise}
*/
attachTo(element) {
return this.renderTo(element);
}
/**
* Display a point in the publication
* The request will be added to the rendering Queue,
* so it will wait until book is opened, rendering started
* so it will wait until publication is opened, rendering started
* and all other rendering tasks have finished to be called.
* @param {string} target Url or EpubCFI
* @return {Promise}
@ -314,29 +339,33 @@ class Rendition {
* @return {Promise}
*/
_display(target){
if (!this.book) {
if (!this.publication) {
return;
}
var isCfiString = this.epubcfi.isCfiString(target);
var displaying = new defer();
var displayed = displaying.promise;
var section;
var moveTo;
const displaying = new defer();
const displayed = displaying.promise;
let moveTo;
this.displaying = displaying;
// Check if this is a book percentage
if (this.book.locations.length() && isFloat(target)) {
target = this.book.locations.cfiFromPercentage(parseFloat(target));
// Check if this is a publication percentage
if (this.publication.locations.length && isFloat(target)) {
target = this.publication.locations.cfiFromPercentage(parseFloat(target));
}
section = this.book.spine.get(target);
if (typeof target === "undefined") {
target = 0;
}
if(!section){
let resource = this.findResource(target);
if(!resource){
displaying.reject(new Error("No Section Found"));
return displayed;
}
const section = new Section(resource);
this.manager.display(section, target)
.then(() => {
displaying.resolve(section);
@ -363,6 +392,29 @@ class Rendition {
return displayed;
}
findResource(target) {
let resource;
if(this.epubcfi.isCfiString(target)) {
let cfi = new EpubCFI(target);
let spineItem = cfi.base.steps[1];
if (spineItem.id) {
return this.publication.uniqueResources.id(spineItem.id);
} else if (spineItem.index) {
return this.publication.uniqueResources.find((r) => {
return r.cfiPos === spineItem.index;
});
}
}
if (isNumber(target)) {
resource = this.publication.sections.get(target);
} else {
resource = this.publication.uniqueResources.get(target);
}
return resource;
}
/*
render(view, show) {
@ -420,7 +472,7 @@ class Rendition {
this.hooks.render.trigger(view, this)
.then(() => {
if (view.contents) {
this.hooks.content.trigger(view.contents, this).then(() => {
this.hooks.content.trigger(view.contents, view.section, this).then(() => {
/**
* Emit that a section has been rendered
* @event rendered
@ -561,11 +613,6 @@ class Rendition {
var minSpreadWidth = this.settings.minSpreadWidth || metadata.minSpreadWidth || 800;
var direction = this.settings.direction || metadata.direction || "ltr";
if ((this.settings.width === 0 || this.settings.width > 0) &&
(this.settings.height === 0 || this.settings.height > 0)) {
// viewport = "width="+this.settings.width+", height="+this.settings.height+"";
}
properties = {
layout : layout,
spread : spread,
@ -629,7 +676,7 @@ class Rendition {
this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => {
this.emit(EVENTS.RENDITION.LAYOUT, props, changed);
})
});
}
if (this.manager && this._layout) {
@ -700,7 +747,7 @@ class Rendition {
this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
index: this.location.start.index,
href: this.location.start.href,
url: this.location.start.url,
start: this.location.start.cfi,
end: this.location.end.cfi,
percentage: this.location.start.percentage
@ -722,7 +769,7 @@ class Rendition {
* @deprecated
* @type {object}
* @property {number} index
* @property {string} href
* @property {string} url
* @property {EpubCFI} start
* @property {EpubCFI} end
* @property {number} percentage
@ -730,7 +777,7 @@ class Rendition {
*/
this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
index: this.location.start.index,
href: this.location.start.href,
url: this.location.start.url,
start: this.location.start.cfi,
end: this.location.end.cfi,
percentage: this.location.start.percentage
@ -780,7 +827,7 @@ class Rendition {
let located = {
start: {
index: start.index,
href: start.href,
url: start.url,
cfi: start.mapping.start,
displayed: {
page: start.pages[0] || 1,
@ -789,7 +836,7 @@ class Rendition {
},
end: {
index: end.index,
href: end.href,
url: end.url,
cfi: end.mapping.end,
displayed: {
page: end.pages[end.pages.length-1] || 1,
@ -798,35 +845,39 @@ class Rendition {
}
};
let locationStart = this.book.locations.locationFromCfi(start.mapping.start);
let locationEnd = this.book.locations.locationFromCfi(end.mapping.end);
if (this.publication.locations.length) {
let locationStart = this.publication.locators.locationFromCfi(start.mapping.start);
let locationEnd = this.publication.locators.locationFromCfi(end.mapping.end);
if (locationStart != null) {
located.start.location = locationStart;
located.start.percentage = this.book.locations.percentageFromLocation(locationStart);
}
if (locationEnd != null) {
located.end.location = locationEnd;
located.end.percentage = this.book.locations.percentageFromLocation(locationEnd);
if (locationStart != null) {
located.start.location = locationStart;
located.start.percentage = this.publication.locators.percentageFromLocation(locationStart);
}
if (locationEnd != null) {
located.end.location = locationEnd;
located.end.percentage = this.publication.locators.percentageFromLocation(locationEnd);
}
}
let pageStart = this.book.pageList.pageFromCfi(start.mapping.start);
let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end);
if (this.publication.pagelist.length) {
let pageStart = this.publication.locators.pageFromCfi(start.mapping.start);
let pageEnd = this.publication.locators.pageFromCfi(end.mapping.end);
if (pageStart != -1) {
located.start.page = pageStart;
}
if (pageEnd != -1) {
located.end.page = pageEnd;
if (pageStart != -1) {
located.start.page = pageStart;
}
if (pageEnd != -1) {
located.end.page = pageEnd;
}
}
if (end.index === this.book.spine.last().index &&
located.end.displayed.page >= located.end.displayed.total) {
if (end.index === this.publication.sections.last().index &&
located.end.displayed.page >= located.end.displayed.total) {
located.atEnd = true;
}
if (start.index === this.book.spine.first().index &&
located.start.displayed.page === 1) {
if (start.index === this.publication.sections.first().index &&
located.start.displayed.page === 1) {
located.atStart = true;
}
@ -838,27 +889,27 @@ class Rendition {
*/
destroy(){
// Clear the queue
// this.q.clear();
// this.q = undefined;
this.q.clear();
this.q = undefined;
this.manager && this.manager.destroy();
this.book = undefined;
this.publication = undefined;
// this.views = null;
// this.hooks.display.clear();
this.hooks.display.clear();
// this.hooks.serialize.clear();
// this.hooks.content.clear();
// this.hooks.layout.clear();
// this.hooks.render.clear();
// this.hooks.show.clear();
// this.hooks = {};
this.hooks.content.clear();
this.hooks.layout.clear();
this.hooks.render.clear();
this.hooks.show.clear();
this.hooks = {};
// this.themes.destroy();
// this.themes = undefined;
this.themes.destroy();
this.themes = undefined;
// this.epubcfi = undefined;
this.epubcfi = undefined;
// this.starting = undefined;
// this.started = undefined;
@ -1001,26 +1052,34 @@ class Rendition {
/**
* Hook to handle link clicks in rendered content
* @param {Contents} contents
* @param {Section} section
* @private
*/
handleLinks(contents) {
handleLinks(contents, section) {
if (contents) {
contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => {
let relative = this.book.path.relative(href);
this.display(relative);
let resolved = this.publication.resolve(href);
this.display(resolved);
});
}
}
preloadImages(contents) {
let doc = contents.document;
let imgs = doc.querySelectorAll("img");
for (const img of imgs) {
img.loading = "lazy";
}
}
/**
* Hook to handle injecting stylesheet before
* a Section is serialized
* @param {document} doc
* @param {Contents} contents
* @param {Section} section
* @private
*/
injectStylesheet(doc, section) {
let style = doc.createElement("link");
style.setAttribute("type", "text/css");
style.setAttribute("rel", "stylesheet");
style.setAttribute("href", this.settings.stylesheet);
@ -1030,7 +1089,7 @@ class Rendition {
/**
* Hook to handle injecting scripts before
* a Section is serialized
* @param {document} doc
* @param {Contents} contents
* @param {Section} section
* @private
*/
@ -1045,20 +1104,68 @@ class Rendition {
/**
* Hook to handle the document identifier before
* a Section is serialized
* @param {document} doc
* @param {Contents} contents
* @param {Section} section
* @private
*/
injectIdentifier(doc, section) {
let ident = this.book.packaging.metadata.identifier;
let meta = doc.createElement("meta");
meta.setAttribute("name", "dc.relation.ispartof");
let ident = this.publication.metadata.identifier;
let isXml = isXMLDocument(doc);
if (ident) {
let meta = isXml ? doc.createElementNS(XML_NS, "meta") : doc.createElement("meta");
meta.setAttribute("name", "dc.relation.ispartof");
meta.setAttribute("content", ident);
doc.getElementsByTagName("head")[0].appendChild(meta);
}
doc.getElementsByTagName("head")[0].appendChild(meta);
}
injectBase(doc, section) {
let head = doc.querySelector("head");
let base = doc.querySelector("base");
let isXml = isXMLDocument(doc);
if (!base) {
base = isXml ? doc.createElementNS(XML_NS, "base") : doc.createElement("base");
head.insertBefore(base, head.firstChild);
}
base.setAttribute("href", section.url);
}
injectCanonical(doc, section) {
let url = this.canonical(section.url);
let head = doc.querySelector("head");
let link = doc.querySelector("link[rel='canonical']");
let isXml = isXMLDocument(doc);
if (!link) {
link = isXml ? doc.createElementNS(XML_NS, "link") : doc.createElement("link");
head.appendChild(link);
}
link.setAttribute("rel", "canonical");
link.setAttribute("href", url);
}
canonical(path) {
let url = path;
if (!path) {
return "";
}
if (this.settings.canonical) {
url = this.settings.canonical(path);
} else {
url = this.publication.resolve(path, true);
}
return url;
}
scale(s) {
return this.manager && this.manager.scale(s);
}
}
//-- Enable binding events to Renderer

View file

@ -1,31 +1,35 @@
import { defer } from "./utils/core";
import EpubCFI from "./epubcfi";
import Hook from "./utils/hook";
import { sprint } from "./utils/core";
import { replaceBase } from "./utils/replacements";
import Request from "./utils/request";
import { DOMParser as XMLDOMSerializer } from "@xmldom/xmldom";
import { defer } from "../utils/core.js";
import EpubCFI from "../utils/epubcfi.js";
import Hook from "../utils/hook.js";
import { sprint, createBase64Url, createBlobUrl, blob2base64, revokeBlobUrl } from "../utils/core.js";
import { replaceBase } from "../utils/replacements.js";
import Request from "../utils/request.js";
// import xmldom from "xmldom";
/**
* Represents a Section of the Book
*
* In most books this is equivalent to a Chapter
* In most books this is equivelent to a Chapter
* @param {object} item The spine item representing the section
* @param {object} hooks hooks for serialize and content
* @param {object} settings
* @param {object} settings.replacements
*/
class Section {
constructor(item, hooks){
constructor(item, hooks, settings){
this.item = item;
this.idref = item.idref;
this.linear = item.linear === "yes";
this.properties = item.properties;
this.index = item.index;
this.href = item.href;
this.url = item.url;
this.source = item.source;
this.canonical = item.canonical;
this.type = item.type;
this.next = item.next;
this.prev = item.prev;
this.cfiBase = item.cfiBase;
this.canonical = item.canonical;
if (hooks) {
this.hooks = hooks;
@ -38,6 +42,10 @@ class Section {
this.document = undefined;
this.contents = undefined;
this.output = undefined;
this.originalHref = undefined;
this.settings = settings || {};
}
/**
@ -53,10 +61,9 @@ class Section {
if(this.contents) {
loading.resolve(this.contents);
} else {
request(this.url)
let type = this.type === "application/xhtml+xml" ? "xhtml" : "html";
request(this.url, type)
.then(function(xml){
// var directory = new Url(this.url).directory;
this.document = xml;
this.contents = xml.documentElement;
@ -93,11 +100,11 @@ class Section {
this.load(_request).
then(function(contents){
var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
var isIE = userAgent.indexOf('Trident') >= 0;
var userAgent = (typeof navigator !== "undefined" && navigator.userAgent) || "";
var isIE = userAgent.indexOf("Trident") >= 0;
var Serializer;
if (typeof XMLSerializer === "undefined" || isIE) {
Serializer = XMLDOMSerializer;
// Serializer = xmldom.XMLSerializer;
} else {
Serializer = XMLSerializer;
}
@ -172,81 +179,12 @@ class Section {
find(node);
});
return matches;
};
/**
* Search a string in multiple sequential Element of the section. If the document.createTreeWalker api is missed(eg: IE8), use `find` as a fallback.
* @param {string} _query The query string to search
* @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5.
* @return {object[]} A list of matches, with form {cfi, excerpt}
*/
search(_query , maxSeqEle = 5){
if (typeof(document.createTreeWalker) == "undefined") {
return this.find(_query);
}
let matches = [];
const excerptLimit = 150;
const section = this;
const query = _query.toLowerCase();
const search = function(nodeList){
const textWithCase = nodeList.reduce((acc ,current)=>{
return acc + current.textContent;
},"");
const text = textWithCase.toLowerCase();
const pos = text.indexOf(query);
if (pos != -1){
const startNodeIndex = 0 , endPos = pos + query.length;
let endNodeIndex = 0 , l = 0;
if (pos < nodeList[startNodeIndex].length){
let cfi;
while( endNodeIndex < nodeList.length - 1 ){
l += nodeList[endNodeIndex].length;
if ( endPos <= l){
break;
}
endNodeIndex += 1;
}
let startNode = nodeList[startNodeIndex] , endNode = nodeList[endNodeIndex];
let range = section.document.createRange();
range.setStart(startNode,pos);
let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc,current)=>{return acc+current.textContent.length;},0) ;
range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount );
cfi = section.cfiFromRange(range);
let excerpt = nodeList.slice(0, endNodeIndex+1).reduce((acc,current)=>{return acc+current.textContent ;},"");
if (excerpt.length > excerptLimit){
excerpt = excerpt.substring(pos - excerptLimit/2, pos + excerptLimit/2);
excerpt = "..." + excerpt + "...";
}
matches.push({
cfi: cfi,
excerpt: excerpt
});
}
}
}
const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false);
let node , nodeList = [];
while (node = treeWalker.nextNode()) {
nodeList.push(node);
if (nodeList.length == maxSeqEle){
search(nodeList.slice(0 , maxSeqEle));
nodeList = nodeList.slice(1, maxSeqEle);
}
}
if (nodeList.length > 0){
search(nodeList);
}
return matches;
}
/**
* Reconciles the current chapters layout properties with
* the global layout properties.
* Reconciles the current chapters layout properies with
* the global layout properities.
* @param {object} globalLayout The global layout settings object, chapter properties string
* @return {object} layoutProperties Object with layout properties
*/
@ -280,7 +218,7 @@ class Section {
* @return {string} cfi an EpubCFI string
*/
cfiFromRange(_range) {
return new EpubCFI(_range, this.cfiBase).toString();
return new EpubCFI(_range, this.canonical).toString();
}
/**
@ -289,7 +227,7 @@ class Section {
* @return {string} cfi an EpubCFI string
*/
cfiFromElement(el) {
return new EpubCFI(el, this.cfiBase).toString();
return new EpubCFI(el, this.canonical).toString();
}
/**
@ -301,22 +239,73 @@ class Section {
this.output = undefined;
}
/**
* Return an object representation of the item
* @return {object}
*/
toObject() {
return {
idref : this.idref,
linear : this.linear ? "yes" : "no",
url : this.url,
source : this.source,
type : this.type,
canonical : this.canonical
}
}
/**
* Create a url from the content
*/
createUrl(request) {
//var parsedUrl = new Url(url);
//var mimeType = mime.lookup(parsedUrl.filename);
let mimeType = this.type;
return this.render(request)
.then((text) => {
return new Blob([text], {type : mimeType});
})
.then((blob) => {
if (this.settings.replacements && this.settings.replacements === "base64") {
return blob2base64(blob)
.then((blob) => {
return createBase64Url(blob, mimeType);
});
} else {
return createBlobUrl(blob, mimeType);
}
})
.then((url) => {
this.originalHref = this.url;
this.url = url;
this.unload();
return url;
})
}
destroy() {
this.unload();
this.hooks.serialize.clear();
this.hooks.content.clear();
if (this.originalHref) {
revokeBlobUrl(this.url);
}
this.hooks = undefined;
this.idref = undefined;
this.linear = undefined;
this.properties = undefined;
this.index = undefined;
this.href = undefined;
this.url = undefined;
this.source = undefined;
this.next = undefined;
this.prev = undefined;
this.cfiBase = undefined;
this.canonical = undefined;
}
}

230
src/rendition/sections.js Normal file
View file

@ -0,0 +1,230 @@
import EpubCFI from "../utils/epubcfi.js";
import Hook from "../utils/hook.js";
import Section from "./section.js";
import { replaceBase, replaceCanonical, replaceMeta } from "../utils/replacements.js";
/**
* A collection of Section Items
*/
class Sections {
constructor(items) {
this.sectionByHref = {};
this.sectionById = {};
this.readingOrder = [];
this.unorderedSections = [];
this.hooks = {};
this.hooks.serialize = new Hook();
this.hooks.content = new Hook();
// Register replacements
this.hooks.content.register(replaceBase);
this.hooks.content.register(replaceCanonical);
this.hooks.content.register(replaceMeta);
this.epubcfi = new EpubCFI();
}
/**
* Get an item from the section
* @param {string|number} [target]
* @return {Section} section
* @example section.get();
* @example section.get(1);
* @example section.get("chap1.html");
* @example section.get("id1234");
*/
get(target) {
let index;
let type;
if (typeof target === "undefined") {
return this.readingOrder.length && this.readingOrder[0]
} else if(this.epubcfi.isCfiString(target)) {
let cfi = new EpubCFI(target);
index = cfi.spinePos;
} else if(typeof target === "number" || isNaN(target) === false){
index = target;
} else if(typeof target === "string" && target.indexOf("#") === 0) {
index = this.sectionById[target.substring(1)];
} else if(typeof target === "string") {
// Remove fragments
target = target.split("#")[0];
if (this.sectionById[target] !== undefined) {
index, type = this.sectionById[target];
} else if (this.sectionById[target] !== undefined) {
index, type = this.sectionByHref[target];
} else {
index, type = this.sectionByHref[encodeURI(target)];
}
}
if (index != undefined && type && type === "unordered") {
return this.unorderedSections[index];
}
if (index != undefined) {
return this.readingOrder[index];
}
}
/**
* Append a Section to the Spine
* @private
* @param {Section} section
*/
append(section) {
const type = "ordered";
const index = this.readingOrder.length;
section.index = index;
this.readingOrder.push(section);
// Encode and Decode href lookups
// see pr for details: https://github.com/futurepress/epub.js/pull/358
this.sectionByHref[decodeURI(section.href)] = { type, index };
this.sectionByHref[encodeURI(section.href)] = { type, index };
this.sectionByHref[section.href] = { type, index };
this.sectionById[section.idref] = { type, index };
return index;
}
/**
* Prepend a Section to the Spine
* @private
* @param {Section} section
*/
prepend(section) {
const type = "ordered";
const index = 0;
// var index = this.sectionItems.unshift(section);
this.sectionByHref[section.href] = { type, index };
this.sectionById[section.idref] = { type, index };
// Re-index
this.readingOrder.forEach(function(item, newIndex){
item.index = newIndex;
});
return 0;
}
// insert(section, index) {
//
// };
unordered(section) {
const type = "unordered";
const index = this.unordered.length;
this.unorderedSections.push(section);
// Encode and Decode href lookups
// see pr for details: https://github.com/futurepress/epub.js/pull/358
this.sectionByHref[decodeURI(section.href)] = { type, index };
this.sectionByHref[encodeURI(section.href)] = { type, index };
this.sectionByHref[section.href] = { type, index };
this.sectionById[section.idref] = { type, index };
return index;
}
/**
* Remove a Section from the Spine
* @private
* @param {Section} section
*/
remove(section) {
let orderedIndex = this.readingOrder.indexOf(section);
if (orderedIndex > -1) {
delete this.sectionByHref[section.href];
delete this.sectionById[section.idref];
return this.readingOrder.splice(orderedIndex, 1);
}
let unorderedIndex = this.unordered.indexOf(section);
if (unorderedIndex > -1) {
delete this.sectionByHref[section.href];
delete this.sectionById[section.idref];
return this.unorderedSections.splice(unorderedIndex, 1);
}
}
/**
* Loop over the Sections in the Spine
* @return {method} forEach
*/
each() {
return this.readingOrder.forEach.apply(this.readingOrder, arguments);
}
/**
* Find the first Section in the Spine
* @return {Section} first section
*/
first() {
return this.get(0);
}
/**
* Find the last Section in the Spine
* @return {Section} last section
*/
last() {
let index = this.readingOrder.length-1;
return this.get(index);
}
/**
* Export an Array of all ordered Sections
* @return {array}
*/
toOrderedArray() {
return this.readingOrder.map(function(item, index){
return item.toObject();
});
}
/**
* Export an Array of all unordered Sections
* @return {array}
*/
toUnorderedArray() {
return this.unorderedSections.map(function (item, index) {
return item.toObject();
});
}
toJSON() {
return JSON.stringify({
readingOrder: this.toOrderedArray(),
unordered: this.toUnorderedArray()
});
}
destroy() {
this.each((section) => section.destroy());
this.readingOrder = undefined;
this.unorderedSections = undefined;
this.sectionByHref = undefined;
this.sectionById = undefined;
this.hooks.serialize.clear();
this.hooks.content.clear();
this.hooks = undefined;
this.epubcfi = undefined;
this.length = undefined;
}
}
export default Sections;

View file

@ -1,4 +1,4 @@
import Url from "./utils/url";
import { createUrl } from "../utils/url.js";
/**
* Themes to apply to displayed content
@ -100,7 +100,7 @@ class Themes {
* @param {string} input
*/
registerUrl (name, input) {
var url = new Url(input);
var url = createUrl(input);
this._themes[name] = { "url": url.toString() };
if (this._injected[name] || name == 'default') {
this.update(name);

View file

@ -1,320 +0,0 @@
import {substitute} from "./utils/replacements";
import {createBase64Url, createBlobUrl, blob2base64} from "./utils/core";
import Url from "./utils/url";
import mime from "./utils/mime";
import Path from "./utils/path";
import path from "path-webpack";
/**
* Handle Package Resources
* @class
* @param {Manifest} manifest
* @param {object} [options]
* @param {string} [options.replacements="base64"]
* @param {Archive} [options.archive]
* @param {method} [options.resolver]
*/
class Resources {
constructor(manifest, options) {
this.settings = {
replacements: (options && options.replacements) || "base64",
archive: (options && options.archive),
resolver: (options && options.resolver),
request: (options && options.request)
};
this.process(manifest);
}
/**
* Process resources
* @param {Manifest} manifest
*/
process(manifest){
this.manifest = manifest;
this.resources = Object.keys(manifest).
map(function (key){
return manifest[key];
});
this.replacementUrls = [];
this.html = [];
this.assets = [];
this.css = [];
this.urls = [];
this.cssUrls = [];
this.split();
this.splitUrls();
}
/**
* Split resources by type
* @private
*/
split(){
// HTML
this.html = this.resources.
filter(function (item){
if (item.type === "application/xhtml+xml" ||
item.type === "text/html") {
return true;
}
});
// Exclude HTML
this.assets = this.resources.
filter(function (item){
if (item.type !== "application/xhtml+xml" &&
item.type !== "text/html") {
return true;
}
});
// Only CSS
this.css = this.resources.
filter(function (item){
if (item.type === "text/css") {
return true;
}
});
}
/**
* Convert split resources into Urls
* @private
*/
splitUrls(){
// All Assets Urls
this.urls = this.assets.
map(function(item) {
return item.href;
}.bind(this));
// Css Urls
this.cssUrls = this.css.map(function(item) {
return item.href;
});
}
/**
* Create a url to a resource
* @param {string} url
* @return {Promise<string>} Promise resolves with url string
*/
createUrl (url) {
var parsedUrl = new Url(url);
var mimeType = mime.lookup(parsedUrl.filename);
if (this.settings.archive) {
return this.settings.archive.createUrl(url, {"base64": (this.settings.replacements === "base64")});
} else {
if (this.settings.replacements === "base64") {
return this.settings.request(url, 'blob')
.then((blob) => {
return blob2base64(blob);
})
.then((blob) => {
return createBase64Url(blob, mimeType);
});
} else {
return this.settings.request(url, 'blob').then((blob) => {
return createBlobUrl(blob, mimeType);
})
}
}
}
/**
* Create blob urls for all the assets
* @return {Promise} returns replacement urls
*/
replacements(){
if (this.settings.replacements === "none") {
return new Promise(function(resolve) {
resolve(this.urls);
}.bind(this));
}
var replacements = this.urls.map( (url) => {
var absolute = this.settings.resolver(url);
return this.createUrl(absolute).
catch((err) => {
console.error(err);
return null;
});
});
return Promise.all(replacements)
.then( (replacementUrls) => {
this.replacementUrls = replacementUrls.filter((url) => {
return (typeof(url) === "string");
});
return replacementUrls;
});
}
/**
* Replace URLs in CSS resources
* @private
* @param {Archive} [archive]
* @param {method} [resolver]
* @return {Promise}
*/
replaceCss(archive, resolver){
var replaced = [];
archive = archive || this.settings.archive;
resolver = resolver || this.settings.resolver;
this.cssUrls.forEach(function(href) {
var replacement = this.createCssFile(href, archive, resolver)
.then(function (replacementUrl) {
// switch the url in the replacementUrls
var indexInUrls = this.urls.indexOf(href);
if (indexInUrls > -1) {
this.replacementUrls[indexInUrls] = replacementUrl;
}
}.bind(this))
replaced.push(replacement);
}.bind(this));
return Promise.all(replaced);
}
/**
* Create a new CSS file with the replaced URLs
* @private
* @param {string} href the original css file
* @return {Promise} returns a BlobUrl to the new CSS file or a data url
*/
createCssFile(href){
var newUrl;
if (path.isAbsolute(href)) {
return new Promise(function(resolve){
resolve();
});
}
var absolute = this.settings.resolver(href);
// Get the text of the css file from the archive
var textResponse;
if (this.settings.archive) {
textResponse = this.settings.archive.getText(absolute);
} else {
textResponse = this.settings.request(absolute, "text");
}
// Get asset links relative to css file
var relUrls = this.urls.map( (assetHref) => {
var resolved = this.settings.resolver(assetHref);
var relative = new Path(absolute).relative(resolved);
return relative;
});
if (!textResponse) {
// file not found, don't replace
return new Promise(function(resolve){
resolve();
});
}
return textResponse.then( (text) => {
// Replacements in the css text
text = substitute(text, relUrls, this.replacementUrls);
// Get the new url
if (this.settings.replacements === "base64") {
newUrl = createBase64Url(text, "text/css");
} else {
newUrl = createBlobUrl(text, "text/css");
}
return newUrl;
}, (err) => {
// handle response errors
return new Promise(function(resolve){
resolve();
});
});
}
/**
* Resolve all resources URLs relative to an absolute URL
* @param {string} absolute to be resolved to
* @param {resolver} [resolver]
* @return {string[]} array with relative Urls
*/
relativeTo(absolute, resolver){
resolver = resolver || this.settings.resolver;
// Get Urls relative to current sections
return this.urls.
map(function(href) {
var resolved = resolver(href);
var relative = new Path(absolute).relative(resolved);
return relative;
}.bind(this));
}
/**
* Get a URL for a resource
* @param {string} path
* @return {string} url
*/
get(path) {
var indexInUrls = this.urls.indexOf(path);
if (indexInUrls === -1) {
return;
}
if (this.replacementUrls.length) {
return new Promise(function(resolve, reject) {
resolve(this.replacementUrls[indexInUrls]);
}.bind(this));
} else {
return this.createUrl(path);
}
}
/**
* Substitute urls in content, with replacements,
* relative to a url if provided
* @param {string} content
* @param {string} [url] url to resolve to
* @return {string} content with urls substituted
*/
substitute(content, url) {
var relUrls;
if (url) {
relUrls = this.relativeTo(url);
} else {
relUrls = this.urls;
}
return substitute(content, relUrls, this.replacementUrls);
}
destroy() {
this.settings = undefined;
this.manifest = undefined;
this.resources = undefined;
this.replacementUrls = undefined;
this.html = undefined;
this.assets = undefined;
this.css = undefined;
this.urls = undefined;
this.cssUrls = undefined;
}
}
export default Resources;

673
src/server/resources.js Normal file
View file

@ -0,0 +1,673 @@
import { substitute } from "../utils/replacements.js";
import { createBlob, createBase64Url, createBlobUrl, blob2base64, revokeBlobUrl, defer } from "../utils/core.js";
import Url from "../utils/url.js";
import { lookup } from "../utils/mime.js";
import Path from "../utils/path.js";
// import path from "path-webpack";
/**
* Handles Package Resources
* @class
* @param {object} resources
* @param {object} [options]
* @param {string} [options.replacements="base64"]
* @param {Archive} [options.archive]
* @param {method} [options.load]
* @param {string} [options.url]
* @param {string} [options.inject]
*/
class Resources {
constructor(resources, options) {
this.settings = {
replacements: (options && options.replacements) || "blobUrl",
archive: (options && options.archive),
load: (options && options.load),
url: (options && options.url),
// path: (options && options.path),
inject: (options && options.inject) || {},
};
this.urlCache = {};
this.resources = Object.assign({}, resources);
this.resourcesByHref = {};
this.ids = [];
this.html = [];
this.assets = [];
this.css = [];
if (typeof(this.settings.url) === "string") {
this.url = new Url(this.settings.url);
this.path = new Path(this.settings.url);
} else if(typeof(this.settings.url) === "object") {
this.url = this.settings.url;
this.path = new Path(this.url.toString());
} else {
this.path = new Path("/");
}
if (resources) {
this.split(resources);
}
}
/**
* Split resources by type
* @private
*/
split(resources){
let keys = Object.keys(resources);
// HTML
let html = keys.
filter(function (key){
let item = resources[key];
if (item.type === "application/xhtml+xml" ||
item.type === "text/html") {
return true;
}
});
// Exclude HTML & CSS
let assets = keys.
filter(function (key){
let item = resources[key];
if (item.type !== "application/xhtml+xml" &&
item.type !== "text/html" &&
item.type !== "text/css") {
return true;
}
});
// Only CSS
let css = keys.
filter(function (key){
let item = resources[key];
if (item.type === "text/css") {
return true;
}
});
keys.forEach((id) => {
let resource = resources[id];
// set ID from keys
resource.id = id;
if (!resource.source) {
resource.source = resource.href;
}
this.resourcesByHref[resource.href] = id;
});
this.ids = keys;
this.html = html;
this.assets = assets;
this.css = css;
return {
html,
assets,
css
}
}
/**
* Save all resources into the cache
* @return {array}
*/
cache(key, origin) {
if (typeof(caches) === "undefined") {
return new Promise(function(resolve, reject) {
resolve([]);
});
}
this.cacheKey = key;
let originUrl = this.url;
if (typeof(origin) === "string") {
originUrl = new Url(origin);
}
this.ids.map((resourceId) => {
let resource = this.resources[resourceId];
let href = resource.source || resource.href;
let isAbsolute = href.indexOf("://") > -1;
let path = isAbsolute ? href : this.path.resolve(href);
let url;
if (!isAbsolute && originUrl) {
url = originUrl.resolve(href);
} else {
let originalUrl = new Url(href, origin);
let base = encodeURIComponent(originalUrl.origin);
path = path.replace(originalUrl.origin, "");
url = new Url(key + base + path, location.href).toString();
}
this.resources[resourceId].path = path;
this.resources[resourceId].cached = url;
this.urlCache[path] = url;
});
return caches.open(key).then((cache) => {
let urls = this.ids.map((resourceId) => {
let resource = this.resources[resourceId];
let url = resource.cached;
let path = resource.path;
let mimeType = lookup(path);
return cache.match(url)
.then((result) => {
if (!result) {
let loaded;
if (resource.type === "application/xhtml+xml" ||
resource.type === "text/html") {
loaded = this.settings.load(path, "text").then((text) => {
if (this.settings.inject.identifier) {
text = this.injectIdentifier(text, this.settings.inject.identifier);
}
if (this.settings.inject.script) {
text = this.injectScript(text, this.settings.inject.script);
}
if (this.settings.inject.stylesheet) {
text = this.injectStylesheet(text, this.settings.inject.script);
}
return createBlob(text, resource.type);
});
} else {
loaded = this.settings.load(path, "blob");
}
return loaded.then((blob) => {
let response = new Response(blob, {
"status" : 200,
"headers": { 'Content-Type': mimeType }
});
this.urlCache[path] = url;
return cache.put(url, response);
}, (err) => {
console.warn("Missing Resource", path);
return path;
}).then(() => {
return url;
});
} else {
this.urlCache[path] = url;
return url;
}
});
});
return Promise.all(urls);
});
}
/**
* Create blob urls for all the assets
* @return {Promise} returns replacement urls
*/
replacements(){
if (this.settings.replacements === "none") {
return new Promise(function(resolve) {
resolve([]);
}.bind(this));
}
var replacements = [];
// Replace all the assets
let assets = this.assets.
map( (resourceId) => {
let url = this.replacementUrl(resourceId);
replacements.push(url);
return url;
});
// Re-write and replace css files
let css = Promise.all(assets).then(() => {
return this.css.
map( (resourceId) => {
let url = this.replacementCss(resourceId);
replacements.push(url);
return url;
});
});
// Re-write and replace htmls files
let html = css.then(() => {
return this.html.
map( (resourceId) => {
let url = this.replacementHtml(resourceId);
replacements.push(url);
return url;
});
});
return html.then(() => {
return Promise.all(replacements);
}).then((urls) => {
return urls;
});
}
/**
* Create a replacement url from a resource
* @param {number} resourceId
* @return {promise}
*/
replacementUrl(resourceId) {
let resource = this.resources[resourceId];
let absolute = this.url.resolve(resource.href);
let createUrl;
if (this.settings.replacements === "base64") {
createUrl = this.base64UrlFrom(absolute);
} else {
createUrl = this.blobUrlFrom(absolute);
}
return createUrl
.then((url) => {
this.resources[resourceId].replacement = url;
this.urlCache[absolute] = url;
return url;
})
.catch((err) => {
console.error(err);
return null;
});
}
/**
* Replace URLs in CSS resources
* @private
* @param {number} resourceId
* @return {Promise}
*/
replacementCss(resourceId){
let newUrl;
let resource = this.resources[resourceId];
let href = resource.href;
if (this.path.isAbsolute(href)) {
return new Promise(function(resolve){
resolve(href);
});
}
let resolved = this.path.resolve(href);
let fullpath = new Path(resolved);
// Get the text of the css file from the archive
var textResponse;
if (this.settings.archive) {
textResponse = this.settings.archive.getText(resolved);
} else {
textResponse = this.settings.load(resolved, "text");
}
return textResponse.then( (text) => {
let replacements = {};
// Get asset links relative to css file
this.ids.forEach( (resourceId) => {
let resource = this.resources[resourceId];
if (!resource.replacement) {
return
}
let assetHref = resource.href;
let resolved = this.path.resolve(assetHref);
let relative = fullpath.relative(resolved);
replacements[relative] = resource.replacement;
});
// Replacements in the css text
text = this.substitute(text, replacements);
// Get the new url
if (this.settings.replacements === "base64") {
newUrl = createBase64Url(text, "text/css");
} else {
newUrl = createBlobUrl(text, "text/css");
}
return newUrl;
}, (err) => {
// handle response errors
return new Promise(function(resolve){
resolve();
});
}).then((url) => {
if (url) {
this.resources[resourceId].replacement = url;
this.urlCache[fullpath] = url;
}
return url;
});
}
/**
* Replace URLs in HTML resources
* @private
* @param {number} resourceId
* @return {Promise}
*/
replacementHtml(resourceId){
let newUrl;
let resource = this.resources[resourceId];
let href = resource.href;
let mimeType = lookup(href);
if (this.path.isAbsolute(href)) {
return new Promise(function(resolve){
resolve(href);
});
}
let resolved = this.path.resolve(href);
let fullpath = new Path(resolved);
// Get the text of the css file from the archive
var textResponse;
if (this.settings.archive) {
textResponse = this.settings.archive.getText(resolved);
} else {
textResponse = this.settings.load(resolved, "text");
}
return textResponse.then( (text) => {
let replacements = {};
// Get asset links relative to html file
this.ids.forEach( (resourceId) => {
let resource = this.resources[resourceId];
if (!resource.replacement) {
return
}
let assetHref = resource.href;
let resolved = this.path.resolve(assetHref);
let relative = fullpath.relative(resolved);
replacements[relative] = resource.replacement;
});
// Replacements in the css text
text = this.substitute(text, replacements);
// Inject
if (this.settings.inject.base) {
text = this.injectBase(text, this.settings.inject.base);
}
if (this.settings.inject.identifier) {
text = this.injectIdentifier(text, this.settings.inject.identifier);
}
if (this.settings.inject.script) {
text = this.injectScript(text, this.settings.inject.script);
}
if (this.settings.inject.stylesheet) {
text = this.injectStylesheet(text, this.settings.inject.script);
}
// Get the new url
if (this.settings.replacements === "base64") {
newUrl = createBase64Url(text, mimeType);
} else {
newUrl = createBlobUrl(text, mimeType);
}
return newUrl;
}, (err) => {
// handle response errors
return new Promise(function(resolve){
resolve();
});
}).then((url) => {
if (url) {
this.resources[resourceId].replacement = url;
this.urlCache[fullpath] = url;
}
return url;
});
}
/**
* Create a blob url from a resource absolute url
* @param {string} url
* @return {string} the resolved path string
*/
blobUrlFrom (url) {
var parsedUrl = new Url(url);
var mimeType = lookup(parsedUrl.filename);
if (this.settings.archive) {
return this.settings.archive.createUrl(url, {"base64": false});
} else {
return this.settings.load(url, "blob").then((blob) => {
return createBlobUrl(blob, mimeType);
});
}
}
/**
* Create a base64 encoded url from a resource absolute url
* @param {string} url
* @return {string} the resolved path string
*/
base64UrlFrom (url) {
var parsedUrl = new Url(url);
var mimeType = lookup(parsedUrl.filename);
if (this.settings.archive) {
return this.settings.archive.createUrl(url, {"base64": true});
} else {
return this.settings.load(url, "blob")
.then((blob) => {
return blob2base64(blob);
})
.then((blob) => {
return createBase64Url(blob, mimeType);
});
}
}
/**
* Substitute urls in a resource
*/
substitute(text, resources) {
let query = Object.keys(resources).map((i) => {
return i.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
}).join("|");
let reg = new RegExp("(" + query + ")", "g");
return text.replace(reg, function(match) {
return resources[match];
});
}
injectStylesheet(text, src) {
let reg = /<[ ]*\/head[ ]*>/;
let toInject = `<link href="${src}" rel="stylesheet" />`;
return text.replace(reg, toInject + "$&");
}
injectScript(text, src) {
let reg = /<[ ]*\/head[ ]*>/;
let toInject = `<script src="${src}" type="text/javascript"></script>`;
return text.replace(reg, toInject + "$&");
}
injectIdentifier(text, identifier) {
let reg = /<[ ]*\/head[ ]*>/;
let toInject = `<meta name="dc.relation.ispartof" content="${identifier}" />`;
return text.replace(reg, toInject + "$&");
}
injectBase(text, url) {
let reg = /<[ ]*head[ ]*>/;
let absolute = (url.indexOf("://") > -1);
// Fix for Safari crashing if the url doesn't have an origin
if (!absolute && (typeof(window) !== "undefined" && window.location)) {
let parts = window.location.href.split("/")
let directory = "";
parts.pop();
directory = parts.join("/");
url = directory + url;
}
let toInject = `<base href="${url}" />`;
return text.replace(reg, "$&" + toInject);
}
origin(url) {
this.url = new Url(url);
}
/**
* Resolve a path to its absolute url (or replaced url)
* @param {string} path
* @return {string} the resolved path string
*/
resolve(path) {
if (!path) {
return;
}
let isAbsolute = path.indexOf("://") > -1;
let href = isAbsolute ? path : this.path.resolve(path);
let resolved = href;
let search = href.split("?");
let anchor = href.split("#");
let base = href;
if (search.length > 1) {
base = search[0];
} else if (anchor.length > 1) {
base = anchor[0];
}
let cached = this.urlCache[base];
if (cached) {
resolved = cached;
// Add query strings back
if (search.length > 1) {
resolved += "?" + search[1];
} else if (anchor.length > 1) {
resolved += "#" + anchor[1];
}
} else if (this.url) {
resolved = this.url.resolve(path);
} else {
resolved = path;
}
return resolved;
}
/**
* Export an Array of all resources
* @return {array}
*/
toArray() {
return this.ids.map((key) => {
let item = this.resources[key];
let { type, properties, id } = item;
let source = item.href;
let href = item.cached || item.replacement || (this.url && this.url.resolve(item.href)) || item.href;
return {
href,
source,
type,
properties,
id
};
});
}
forEach(func) {
return this.ids.
forEach((id) => {
let r = this.resources[id];
r.id = key;
func(r);
});
}
map(func) {
return this.ids.
map((id) => {
let r = this.resources[id];
r.id = key;
return func(r);
});
}
filter(func) {
return this.ids.
filter((id) => {
let r = this.resources[id];
r.id = key;
return func(r);
});
}
get(what) {
if (what in this.resources) {
return this.resources[what];
} else if (what in this.resourcesByHref) {
let id = this.resourcesByHref[what];
return this.resources[id];
}
}
revokeBlobUrls() {
this.ids.forEach((id) => {
let r = this.resources[id];
if (r.replacement) {
revokeBlobUrl(r.replacement);
}
});
}
destroy() {
this.revokeBlobUrls();
this.settings = undefined;
this.manifest = undefined;
this.html = undefined;
this.assets = undefined;
this.css = undefined;
this.urls = undefined;
this.cssUrls = undefined;
}
}
export default Resources;

View file

@ -1,274 +0,0 @@
import EpubCFI from "./epubcfi";
import Hook from "./utils/hook";
import Section from "./section";
import {replaceBase, replaceCanonical, replaceMeta} from "./utils/replacements";
/**
* A collection of Spine Items
*/
class Spine {
constructor() {
this.spineItems = [];
this.spineByHref = {};
this.spineById = {};
this.hooks = {};
this.hooks.serialize = new Hook();
this.hooks.content = new Hook();
// Register replacements
this.hooks.content.register(replaceBase);
this.hooks.content.register(replaceCanonical);
this.hooks.content.register(replaceMeta);
this.epubcfi = new EpubCFI();
this.loaded = false;
this.items = undefined;
this.manifest = undefined;
this.spineNodeIndex = undefined;
this.baseUrl = undefined;
this.length = undefined;
}
/**
* Unpack items from a opf into spine items
* @param {Packaging} _package
* @param {method} resolver URL resolver
* @param {method} canonical Resolve canonical url
*/
unpack(_package, resolver, canonical) {
this.items = _package.spine;
this.manifest = _package.manifest;
this.spineNodeIndex = _package.spineNodeIndex;
this.baseUrl = _package.baseUrl || _package.basePath || "";
this.length = this.items.length;
this.items.forEach( (item, index) => {
var manifestItem = this.manifest[item.idref];
var spineItem;
item.index = index;
item.cfiBase = this.epubcfi.generateChapterComponent(this.spineNodeIndex, item.index, item.id);
if (item.href) {
item.url = resolver(item.href, true);
item.canonical = canonical(item.href);
}
if(manifestItem) {
item.href = manifestItem.href;
item.url = resolver(item.href, true);
item.canonical = canonical(item.href);
if(manifestItem.properties.length){
item.properties.push.apply(item.properties, manifestItem.properties);
}
}
if (item.linear === "yes") {
item.prev = function() {
let prevIndex = item.index;
while (prevIndex > 0) {
let prev = this.get(prevIndex-1);
if (prev && prev.linear) {
return prev;
}
prevIndex -= 1;
}
return;
}.bind(this);
item.next = function() {
let nextIndex = item.index;
while (nextIndex < this.spineItems.length-1) {
let next = this.get(nextIndex+1);
if (next && next.linear) {
return next;
}
nextIndex += 1;
}
return;
}.bind(this);
} else {
item.prev = function() {
return;
}
item.next = function() {
return;
}
}
spineItem = new Section(item, this.hooks);
this.append(spineItem);
});
this.loaded = true;
}
/**
* Get an item from the spine
* @param {string|number} [target]
* @return {Section} section
* @example spine.get();
* @example spine.get(1);
* @example spine.get("chap1.html");
* @example spine.get("#id1234");
*/
get(target) {
var index = 0;
if (typeof target === "undefined") {
while (index < this.spineItems.length) {
let next = this.spineItems[index];
if (next && next.linear) {
break;
}
index += 1;
}
} else if(this.epubcfi.isCfiString(target)) {
let cfi = new EpubCFI(target);
index = cfi.spinePos;
} else if(typeof target === "number" || isNaN(target) === false){
index = target;
} else if(typeof target === "string" && target.indexOf("#") === 0) {
index = this.spineById[target.substring(1)];
} else if(typeof target === "string") {
// Remove fragments
target = target.split("#")[0];
index = this.spineByHref[target] || this.spineByHref[encodeURI(target)];
}
return this.spineItems[index] || null;
}
/**
* Append a Section to the Spine
* @private
* @param {Section} section
*/
append(section) {
var index = this.spineItems.length;
section.index = index;
this.spineItems.push(section);
// Encode and Decode href lookups
// see pr for details: https://github.com/futurepress/epub.js/pull/358
this.spineByHref[decodeURI(section.href)] = index;
this.spineByHref[encodeURI(section.href)] = index;
this.spineByHref[section.href] = index;
this.spineById[section.idref] = index;
return index;
}
/**
* Prepend a Section to the Spine
* @private
* @param {Section} section
*/
prepend(section) {
// var index = this.spineItems.unshift(section);
this.spineByHref[section.href] = 0;
this.spineById[section.idref] = 0;
// Re-index
this.spineItems.forEach(function(item, index){
item.index = index;
});
return 0;
}
// insert(section, index) {
//
// };
/**
* Remove a Section from the Spine
* @private
* @param {Section} section
*/
remove(section) {
var index = this.spineItems.indexOf(section);
if(index > -1) {
delete this.spineByHref[section.href];
delete this.spineById[section.idref];
return this.spineItems.splice(index, 1);
}
}
/**
* Loop over the Sections in the Spine
* @return {method} forEach
*/
each() {
return this.spineItems.forEach.apply(this.spineItems, arguments);
}
/**
* Find the first Section in the Spine
* @return {Section} first section
*/
first() {
let index = 0;
do {
let next = this.get(index);
if (next && next.linear) {
return next;
}
index += 1;
} while (index < this.spineItems.length) ;
}
/**
* Find the last Section in the Spine
* @return {Section} last section
*/
last() {
let index = this.spineItems.length-1;
do {
let prev = this.get(index);
if (prev && prev.linear) {
return prev;
}
index -= 1;
} while (index >= 0);
}
destroy() {
this.each((section) => section.destroy());
this.spineItems = undefined
this.spineByHref = undefined
this.spineById = undefined
this.hooks.serialize.clear();
this.hooks.content.clear();
this.hooks = undefined;
this.epubcfi = undefined;
this.loaded = false;
this.items = undefined;
this.manifest = undefined;
this.spineNodeIndex = undefined;
this.baseUrl = undefined;
this.length = undefined;
}
}
export default Spine;

View file

@ -1,384 +0,0 @@
import {defer, isXml, parse} from "./utils/core";
import httpRequest from "./utils/request";
import mime from "./utils/mime";
import Path from "./utils/path";
import EventEmitter from "event-emitter";
import localforage from "localforage";
/**
* Handles saving and requesting files from local storage
* @class
* @param {string} name This should be the name of the application for modals
* @param {function} [requester]
* @param {function} [resolver]
*/
class Store {
constructor(name, requester, resolver) {
this.urlCache = {};
this.storage = undefined;
this.name = name;
this.requester = requester || httpRequest;
this.resolver = resolver;
this.online = true;
this.checkRequirements();
this.addListeners();
}
/**
* Checks to see if localForage exists in global namspace,
* Requires localForage if it isn't there
* @private
*/
checkRequirements(){
try {
let store;
if (typeof localforage === "undefined") {
store = localforage;
}
this.storage = store.createInstance({
name: this.name
});
} catch (e) {
throw new Error("localForage lib not loaded");
}
}
/**
* Add online and offline event listeners
* @private
*/
addListeners() {
this._status = this.status.bind(this);
window.addEventListener('online', this._status);
window.addEventListener('offline', this._status);
}
/**
* Remove online and offline event listeners
* @private
*/
removeListeners() {
window.removeEventListener('online', this._status);
window.removeEventListener('offline', this._status);
this._status = undefined;
}
/**
* Update the online / offline status
* @private
*/
status(event) {
let online = navigator.onLine;
this.online = online;
if (online) {
this.emit("online", this);
} else {
this.emit("offline", this);
}
}
/**
* Add all of a book resources to the store
* @param {Resources} resources book resources
* @param {boolean} [force] force resaving resources
* @return {Promise<object>} store objects
*/
add(resources, force) {
let mapped = resources.resources.map((item) => {
let { href } = item;
let url = this.resolver(href);
let encodedUrl = window.encodeURIComponent(url);
return this.storage.getItem(encodedUrl).then((item) => {
if (!item || force) {
return this.requester(url, "binary")
.then((data) => {
return this.storage.setItem(encodedUrl, data);
});
} else {
return item;
}
});
});
return Promise.all(mapped);
}
/**
* Put binary data from a url to storage
* @param {string} url a url to request from storage
* @param {boolean} [withCredentials]
* @param {object} [headers]
* @return {Promise<Blob>}
*/
put(url, withCredentials, headers) {
let encodedUrl = window.encodeURIComponent(url);
return this.storage.getItem(encodedUrl).then((result) => {
if (!result) {
return this.requester(url, "binary", withCredentials, headers).then((data) => {
return this.storage.setItem(encodedUrl, data);
});
}
return result;
});
}
/**
* Request a url
* @param {string} url a url to request from storage
* @param {string} [type] specify the type of the returned result
* @param {boolean} [withCredentials]
* @param {object} [headers]
* @return {Promise<Blob | string | JSON | Document | XMLDocument>}
*/
request(url, type, withCredentials, headers){
if (this.online) {
// From network
return this.requester(url, type, withCredentials, headers).then((data) => {
// save to store if not present
this.put(url);
return data;
})
} else {
// From store
return this.retrieve(url, type);
}
}
/**
* Request a url from storage
* @param {string} url a url to request from storage
* @param {string} [type] specify the type of the returned result
* @return {Promise<Blob | string | JSON | Document | XMLDocument>}
*/
retrieve(url, type) {
var deferred = new defer();
var response;
var path = new Path(url);
// If type isn't set, determine it from the file extension
if(!type) {
type = path.extension;
}
if(type == "blob"){
response = this.getBlob(url);
} else {
response = this.getText(url);
}
return response.then((r) => {
var deferred = new defer();
var result;
if (r) {
result = this.handleResponse(r, type);
deferred.resolve(result);
} else {
deferred.reject({
message : "File not found in storage: " + url,
stack : new Error().stack
});
}
return deferred.promise;
});
}
/**
* Handle the response from request
* @private
* @param {any} response
* @param {string} [type]
* @return {any} the parsed result
*/
handleResponse(response, type){
var r;
if(type == "json") {
r = JSON.parse(response);
}
else
if(isXml(type)) {
r = parse(response, "text/xml");
}
else
if(type == "xhtml") {
r = parse(response, "application/xhtml+xml");
}
else
if(type == "html" || type == "htm") {
r = parse(response, "text/html");
} else {
r = response;
}
return r;
}
/**
* Get a Blob from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {Blob}
*/
getBlob(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
return this.storage.getItem(encodedUrl).then(function(uint8array) {
if(!uint8array) return;
mimeType = mimeType || mime.lookup(url);
return new Blob([uint8array], {type : mimeType});
});
}
/**
* Get Text from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {string}
*/
getText(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
mimeType = mimeType || mime.lookup(url);
return this.storage.getItem(encodedUrl).then(function(uint8array) {
var deferred = new defer();
var reader = new FileReader();
var blob;
if(!uint8array) return;
blob = new Blob([uint8array], {type : mimeType});
reader.addEventListener("loadend", () => {
deferred.resolve(reader.result);
});
reader.readAsText(blob, mimeType);
return deferred.promise;
});
}
/**
* Get a base64 encoded result from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {string} base64 encoded
*/
getBase64(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
mimeType = mimeType || mime.lookup(url);
return this.storage.getItem(encodedUrl).then((uint8array) => {
var deferred = new defer();
var reader = new FileReader();
var blob;
if(!uint8array) return;
blob = new Blob([uint8array], {type : mimeType});
reader.addEventListener("loadend", () => {
deferred.resolve(reader.result);
});
reader.readAsDataURL(blob, mimeType);
return deferred.promise;
});
}
/**
* Create a Url from a stored item
* @param {string} url
* @param {object} [options.base64] use base64 encoding or blob url
* @return {Promise} url promise with Url string
*/
createUrl(url, options){
var deferred = new defer();
var _URL = window.URL || window.webkitURL || window.mozURL;
var tempUrl;
var response;
var useBase64 = options && options.base64;
if(url in this.urlCache) {
deferred.resolve(this.urlCache[url]);
return deferred.promise;
}
if (useBase64) {
response = this.getBase64(url);
if (response) {
response.then(function(tempUrl) {
this.urlCache[url] = tempUrl;
deferred.resolve(tempUrl);
}.bind(this));
}
} else {
response = this.getBlob(url);
if (response) {
response.then(function(blob) {
tempUrl = _URL.createObjectURL(blob);
this.urlCache[url] = tempUrl;
deferred.resolve(tempUrl);
}.bind(this));
}
}
if (!response) {
deferred.reject({
message : "File not found in storage: " + url,
stack : new Error().stack
});
}
return deferred.promise;
}
/**
* Revoke Temp Url for a archive item
* @param {string} url url of the item in the store
*/
revokeUrl(url){
var _URL = window.URL || window.webkitURL || window.mozURL;
var fromCache = this.urlCache[url];
if(fromCache) _URL.revokeObjectURL(fromCache);
}
destroy() {
var _URL = window.URL || window.webkitURL || window.mozURL;
for (let fromCache in this.urlCache) {
_URL.revokeObjectURL(fromCache);
}
this.urlCache = {};
this.removeListeners();
}
}
EventEmitter(Store.prototype);
export default Store;

View file

@ -1,62 +1,61 @@
export const EPUBJS_VERSION = "0.3";
export const EPUBJS_VERSION = "0.4";
// Dom events to listen for
export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "mousemove", "click", "touchend", "touchstart", "touchmove"];
export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
export const EVENTS = {
BOOK : {
OPEN_FAILED : "openFailed"
},
CONTENTS : {
EXPAND : "expand",
RESIZE : "resize",
SELECTED : "selected",
SELECTED_RANGE : "selectedRange",
LINK_CLICKED : "linkClicked"
},
LOCATIONS : {
CHANGED : "changed"
},
MANAGERS : {
RESIZE : "resize",
RESIZED : "resized",
ORIENTATION_CHANGE : "orientationchange",
ADDED : "added",
SCROLL : "scroll",
SCROLLED : "scrolled",
REMOVED : "removed",
},
VIEWS : {
AXIS: "axis",
WRITING_MODE: "writingMode",
LOAD_ERROR : "loaderror",
RENDERED : "rendered",
RESIZED : "resized",
DISPLAYED : "displayed",
SHOWN : "shown",
HIDDEN : "hidden",
MARK_CLICKED : "markClicked"
},
RENDITION : {
STARTED : "started",
ATTACHED : "attached",
DISPLAYED : "displayed",
DISPLAY_ERROR : "displayerror",
RENDERED : "rendered",
REMOVED : "removed",
RESIZED : "resized",
ORIENTATION_CHANGE : "orientationchange",
LOCATION_CHANGED : "locationChanged",
RELOCATED : "relocated",
MARK_CLICKED : "markClicked",
SELECTED : "selected",
LAYOUT: "layout"
},
LAYOUT : {
UPDATED : "updated"
},
ANNOTATION : {
ATTACH : "attach",
DETACH : "detach"
}
}
BOOK : {
OPEN_FAILED : "openFailed",
READY : "ready"
},
CONTENTS : {
EXPAND : "expand",
RESIZE : "resize",
SELECTED : "selected",
SELECTED_RANGE : "selectedRange",
LINK_CLICKED : "linkClicked"
},
LOCATIONS : {
CHANGED : "changed"
},
MANAGERS : {
RESIZE : "resize",
RESIZED : "resized",
ORIENTATION_CHANGE : "orientationchange",
ADDED : "added",
SCROLL : "scroll",
SCROLLED : "scrolled"
},
VIEWS : {
AXIS : "axis",
LOAD_ERROR : "loaderror",
RENDERED : "rendered",
RESIZED : "resized",
DISPLAYED : "displayed",
SHOWN : "shown",
HIDDEN : "hidden",
MARK_CLICKED : "markClicked"
},
RENDITION : {
STARTED : "started",
ATTACHED : "attached",
DISPLAYED : "displayed",
DISPLAY_ERROR : "displayerror",
RENDERED : "rendered",
REMOVED : "removed",
RESIZED : "resized",
ORIENTATION_CHANGE : "orientationchange",
LOCATION_CHANGED : "locationChanged",
RELOCATED : "relocated",
MARK_CLICKED : "markClicked",
SELECTED : "selected",
LAYOUT: "layout",
WORKER_FAILED: "workerFailed",
WORKER_INACTIVE: "workerInactive"
},
LAYOUT : {
UPDATED : "updated"
}
};
export const XML_NS = "http://www.w3.org/1999/xhtml";

View file

@ -1,8 +1,9 @@
/**
* Core Utilities and Helpers
* @module Core
*/
import { DOMParser as XMLDOMParser } from "@xmldom/xmldom";
*/
import { XML_NS } from "./constants.js";
/**
* Vendor prefixed requestAnimationFrame
@ -10,11 +11,10 @@ import { DOMParser as XMLDOMParser } from "@xmldom/xmldom";
* @memberof Core
*/
export const requestAnimationFrame = (typeof window != "undefined") ? (window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame) : false;
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const COMMENT_NODE = 8;
const DOCUMENT_NODE = 9;
const _URL = typeof URL != "undefined" ? URL : (typeof window != "undefined" ? (window.URL || window.webkitURL || window.mozURL) : undefined);
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
export const DOCUMENT_NODE = 9;
/**
* Generates a UUID
@ -24,12 +24,14 @@ const _URL = typeof URL != "undefined" ? URL : (typeof window != "undefined" ? (
*/
export function uuid() {
var d = new Date().getTime();
var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=="x" ? r : (r&0x7|0x8)).toString(16);
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
d += performance.now(); //use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
/**
@ -39,11 +41,11 @@ export function uuid() {
*/
export function documentHeight() {
return Math.max(
document.documentElement.clientHeight,
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight
document.documentElement.clientHeight,
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight
);
}
@ -92,18 +94,18 @@ export function isFloat(n) {
* @memberof Core
*/
export function prefixed(unprefixed) {
var vendors = ["Webkit", "webkit", "Moz", "O", "ms" ];
var vendors = ["Webkit", "webkit", "Moz", "O", "ms"];
var prefixes = ["-webkit-", "-webkit-", "-moz-", "-o-", "-ms-"];
var lower = unprefixed.toLowerCase();
var upper = unprefixed[0].toUpperCase() + unprefixed.slice(1);
var length = vendors.length;
if (typeof(document) === "undefined" || typeof(document.body.style[lower]) != "undefined") {
if (typeof (document) === "undefined" || typeof (document.body.style[unprefixed]) != "undefined") {
return unprefixed;
}
for (var i = 0; i < length; i++) {
if (typeof(document.body.style[prefixes[i] + lower]) != "undefined") {
return prefixes[i] + lower;
if (typeof (document.body.style[vendors[i] + upper]) != "undefined") {
return prefixes[i] + unprefixed;
}
}
@ -135,8 +137,8 @@ export function defaults(obj) {
export function extend(target) {
var sources = [].slice.call(arguments, 1);
sources.forEach(function (source) {
if(!source) return;
Object.getOwnPropertyNames(source).forEach(function(propName) {
if (!source) return;
Object.getOwnPropertyNames(source).forEach(function (propName) {
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
});
@ -174,27 +176,27 @@ export function locationOf(item, array, compareFunction, _start, _end) {
var end = _end || array.length;
var pivot = parseInt(start + (end - start) / 2);
var compared;
if(!compareFunction){
compareFunction = function(a, b) {
if(a > b) return 1;
if(a < b) return -1;
if(a == b) return 0;
if (!compareFunction) {
compareFunction = function (a, b) {
if (a > b) return 1;
if (a < b) return -1;
if (a == b) return 0;
};
}
if(end-start <= 0) {
if (end - start <= 0) {
return pivot;
}
compared = compareFunction(array[pivot], item);
if(end-start === 1) {
if (end - start === 1) {
return compared >= 0 ? pivot : pivot + 1;
}
if(compared === 0) {
if (compared === 0) {
return pivot;
}
if(compared === -1) {
if (compared === -1) {
return locationOf(item, array, compareFunction, pivot, end);
} else{
} else {
return locationOf(item, array, compareFunction, start, pivot);
}
}
@ -215,27 +217,27 @@ export function indexOfSorted(item, array, compareFunction, _start, _end) {
var end = _end || array.length;
var pivot = parseInt(start + (end - start) / 2);
var compared;
if(!compareFunction){
compareFunction = function(a, b) {
if(a > b) return 1;
if(a < b) return -1;
if(a == b) return 0;
if (!compareFunction) {
compareFunction = function (a, b) {
if (a > b) return 1;
if (a < b) return -1;
if (a == b) return 0;
};
}
if(end-start <= 0) {
if (end - start <= 0) {
return -1; // Not found
}
compared = compareFunction(array[pivot], item);
if(end-start === 1) {
if (end - start === 1) {
return compared === 0 ? pivot : -1;
}
if(compared === 0) {
if (compared === 0) {
return pivot; // Found
}
if(compared === -1) {
if (compared === -1) {
return indexOfSorted(item, array, compareFunction, pivot, end);
} else{
} else {
return indexOfSorted(item, array, compareFunction, start, pivot);
}
}
@ -255,11 +257,11 @@ export function bounds(el) {
var width = 0;
var height = 0;
widthProps.forEach(function(prop){
widthProps.forEach(function (prop) {
width += parseFloat(style[prop]) || 0;
});
heightProps.forEach(function(prop){
heightProps.forEach(function (prop) {
height += parseFloat(style[prop]) || 0;
});
@ -286,11 +288,11 @@ export function borders(el) {
var width = 0;
var height = 0;
widthProps.forEach(function(prop){
widthProps.forEach(function (prop) {
width += parseFloat(style[prop]) || 0;
});
heightProps.forEach(function(prop){
heightProps.forEach(function (prop) {
height += parseFloat(style[prop]) || 0;
});
@ -311,7 +313,7 @@ export function borders(el) {
export function nodeBounds(node) {
let elPos;
let doc = node.ownerDocument;
if(node.nodeType == Node.TEXT_NODE){
if (node.nodeType == Node.TEXT_NODE) {
let elRange = doc.createRange();
elRange.selectNodeContents(node);
elPos = elRange.getBoundingClientRect();
@ -322,7 +324,7 @@ export function nodeBounds(node) {
}
/**
* Find the equivalent of getBoundingClientRect of a browser window
* Find the equivelent of getBoundingClientRect of a browser window
* @returns {{ width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }}
* @memberof Core
*/
@ -402,8 +404,10 @@ export function isXml(ext) {
* @returns {Blob}
* @memberof Core
*/
export function createBlob(content, mime){
return new Blob([content], {type : mime });
export function createBlob(content, mime) {
return new Blob([content], {
type: mime
});
}
/**
@ -413,11 +417,11 @@ export function createBlob(content, mime){
* @returns {string} url
* @memberof Core
*/
export function createBlobUrl(content, mime){
export function createBlobUrl(content, mime) {
var tempUrl;
var blob = createBlob(content, mime);
tempUrl = _URL.createObjectURL(blob);
tempUrl = URL.createObjectURL(blob);
return tempUrl;
}
@ -427,8 +431,8 @@ export function createBlobUrl(content, mime){
* @param {string} url
* @memberof Core
*/
export function revokeBlobUrl(url){
return _URL.revokeObjectURL(url);
export function revokeBlobUrl(url) {
return URL.revokeObjectURL(url);
}
/**
@ -438,11 +442,11 @@ export function revokeBlobUrl(url){
* @returns {string} url
* @memberof Core
*/
export function createBase64Url(content, mime){
export function createBase64Url(content, mime) {
var data;
var datauri;
if (typeof(content) !== "string") {
if (typeof (content) !== "string") {
// Only handles strings
return;
}
@ -460,7 +464,7 @@ export function createBase64Url(content, mime){
* @returns {string} type
* @memberof Core
*/
export function type(obj){
export function type(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
@ -468,23 +472,17 @@ export function type(obj){
* Parse xml (or html) markup
* @param {string} markup
* @param {string} mime
* @param {boolean} forceXMLDom force using xmlDom to parse instead of native parser
* @param {DOMParser} parser use xmlDom to parse instead of native parser
* @returns {document} document
* @memberof Core
*/
export function parse(markup, mime, forceXMLDom) {
var doc;
var Parser;
if (typeof DOMParser === "undefined" || forceXMLDom) {
Parser = XMLDOMParser;
} else {
Parser = DOMParser;
}
export function parse(markup, mime, parser) {
let doc;
const Parser = parser || DOMParser;
// Remove byte order mark before parsing
// https://www.w3.org/International/questions/qa-byte-order-mark
if(markup.charCodeAt(0) === 0xFEFF) {
if (markup.charCodeAt(0) === 0xFEFF) {
markup = markup.slice(1);
}
@ -548,12 +546,13 @@ export function qsp(el, sel, props) {
sel += prop + "~='" + props[prop] + "'";
}
sel += "]";
return el.querySelector(sel);
} else {
q = el.getElementsByTagName(sel);
filtered = Array.prototype.slice.call(q, 0).filter(function(el) {
filtered = Array.prototype.slice.call(q, 0).filter(function (el) {
for (var prop in props) {
if(el.getAttribute(prop) === props[prop]){
if (el.getAttribute(prop) === props[prop]) {
return true;
}
}
@ -574,10 +573,10 @@ export function qsp(el, sel, props) {
*/
export function sprint(root, func) {
var doc = root.ownerDocument || root;
if (typeof(doc.createTreeWalker) !== "undefined") {
if (typeof (doc.createTreeWalker) !== "undefined") {
treeWalker(root, func, NodeFilter.SHOW_TEXT);
} else {
walk(root, function(node) {
walk(root, function (node) {
if (node && node.nodeType === 3) { // Node.TEXT_NODE
func(node);
}
@ -590,7 +589,7 @@ export function sprint(root, func) {
* @memberof Core
* @param {element} root element to start with
* @param {function} func function to run on each element
* @param {function | object} filter function or object to filter with
* @param {function | object} filter funtion or object to filter with
*/
export function treeWalker(root, func, filter) {
var treeWalker = document.createTreeWalker(root, filter, null, false);
@ -605,19 +604,19 @@ export function treeWalker(root, func, filter) {
* @param {node} node
* @param {callback} return false for continue,true for break inside callback
*/
export function walk(node,callback){
if(callback(node)){
export function walk(node, callback) {
if (callback(node)) {
return true;
}
node = node.firstChild;
if(node){
do{
let walked = walk(node,callback);
if(walked){
if (node) {
do {
let walked = walk(node, callback);
if (walked) {
return true;
}
node = node.nextSibling;
} while(node);
} while (node);
}
}
@ -628,10 +627,10 @@ export function walk(node,callback){
* @memberof Core
*/
export function blob2base64(blob) {
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
reader.onloadend = function () {
resolve(reader.result);
};
});
@ -653,7 +652,7 @@ export function defer() {
*/
this.resolve = null;
/* A method to reject the associated Promise with the value passed.
/* A method to reject the assocaited Promise with the value passed.
* If the promise is already settled it does nothing.
*
* @param {anything} reason: The reason for the rejection of the Promise.
@ -682,17 +681,17 @@ export function defer() {
* @returns {element[]} elements
* @memberof Core
*/
export function querySelectorByType(html, element, type){
export function querySelectorByType(html, element, type) {
var query;
if (typeof html.querySelector != "undefined") {
query = html.querySelector(`${element}[*|type="${type}"]`);
}
// Handle IE not supporting namespaced epub:type in querySelector
if(!query || query.length === 0) {
if (!query || query.length === 0) {
query = qsa(html, element);
for (var i = 0; i < query.length; i++) {
if(query[i].getAttributeNS("http://www.idpf.org/2007/ops", "type") === type ||
query[i].getAttribute("epub:type") === type) {
if (query[i].getAttributeNS("http://www.idpf.org/2007/ops", "type") === type ||
query[i].getAttribute("epub:type") === type) {
return query[i];
}
}
@ -702,7 +701,7 @@ export function querySelectorByType(html, element, type){
}
/**
* Find direct descendents of an element
* Find direct decendents of an element
* @param {element} el
* @returns {element[]} children
* @memberof Core
@ -730,11 +729,11 @@ export function parents(node) {
for (; node; node = node.parentNode) {
nodes.unshift(node);
}
return nodes
return nodes;
}
/**
* Find all direct descendents of a specific type
* Find all direct decendents of a specific type
* @param {element} el
* @param {string} nodeName
* @param {boolean} [single]
@ -768,7 +767,7 @@ export function filterChildren(el, nodeName, single) {
*/
export function getParentByTagName(node, tagname) {
let parent;
if (node === null || tagname === '') return;
if (node === null || tagname === "") return;
parent = node.parentNode;
while (parent.nodeType === 1) {
if (parent.tagName.toLowerCase() === tagname) {
@ -778,6 +777,44 @@ export function getParentByTagName(node, tagname) {
}
}
/**
* Get the next section in the spine
*/
export function nextSection(section, readingOrder) {
let nextIndex = section.index;
if (nextIndex < readingOrder.length - 1) {
return readingOrder.get(nextIndex + 1);
}
return;
}
/**
* Get the previous section in the spine
*/
export function prevSection(section, readingOrder) {
let prevIndex = section.index;
if (prevIndex > 0) {
return readingOrder.get(prevIndex - 1);
}
return;
}
/**
* Serialize the contents of a document
*/
export function serialize(doc) {
let userAgent = (typeof navigator !== "undefined" && navigator.userAgent) || "";
let isIE = userAgent.indexOf("Trident") >= 0;
let Serializer;
if (typeof XMLSerializer === "undefined" || isIE) {
// Serializer = XMLDom.XMLSerializer;
} else {
Serializer = XMLSerializer;
}
let serializer = new Serializer();
return serializer.serializeToString(doc);
}
/**
* Lightweight Polyfill for DOM Range
* @class
@ -841,9 +878,9 @@ export class RangeObject {
}
selectNodeContents(referenceNode) {
let end = referenceNode.childNodes[referenceNode.childNodes - 1];
// let end = referenceNode.childNodes[referenceNode.childNodes - 1];
let endIndex = (referenceNode.nodeType === 3) ?
referenceNode.textContent.length : parent.childNodes.length;
referenceNode.textContent.length : parent.childNodes.length;
this.setStart(referenceNode, 0);
this.setEnd(referenceNode, endIndex);
}
@ -863,7 +900,7 @@ export class RangeObject {
_checkCollapsed() {
if (this.startContainer === this.endContainer &&
this.startOffset === this.endOffset) {
this.startOffset === this.endOffset) {
this.collapsed = true;
} else {
this.collapsed = false;
@ -874,3 +911,45 @@ export class RangeObject {
// TODO: implement walking between start and end to find text
}
}
export function debounce(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}
export function throttle(func, wait = 300, options = {}) {
let result;
let timeout = null;
let previous = 0;
return (...args) => {
let now = Date.now();
if (!previous && options.leading === false) {
previous = now;
}
let remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(this, args);
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(() => {
previous = (options.leading === false) ? 0 : Date.now();
timeout = null;
result = func.apply(this, args);
}, remaining);
}
return result;
};
}
export function isXMLDocument(doc) {
return doc && doc.documentElement.getAttribute('xmlns') === XML_NS;
}

View file

@ -1,4 +1,4 @@
import {extend, type, findChildren, RangeObject, isNumber} from "./utils/core";
import {extend, type, findChildren, RangeObject, isNumber} from "../utils/core.js";
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
@ -137,7 +137,11 @@ class EpubCFI {
// cfi.spineSegment = cfi.base.steps[1];
// Chapter segment is always the second step
cfi.spinePos = cfi.base.steps[1].index;
if (!cfi.base.steps || cfi.base.steps.length < 2) {
return { spinePos: -1 };
} else {
cfi.spinePos = cfi.base.steps[1].index;
}
return cfi;
}

140
src/utils/eventemitter.js Normal file
View file

@ -0,0 +1,140 @@
const { apply, call } = Function.prototype;
const { create, defineProperty, defineProperties } = Object;
const hasOwnProperty = Object.prototype.hasOwnProperty
const descriptor = { configurable: true, enumerable: false, writable: true };
const on = function (type, listener) {
let data;
if (!hasOwnProperty.call(this, '__ee__')) {
data = descriptor.value = create(null);
defineProperty(this, '__ee__', descriptor);
descriptor.value = null;
} else {
data = this.__ee__;
}
if (!data[type]) data[type] = listener;
else if (typeof data[type] === 'object') data[type].push(listener);
else data[type] = [data[type], listener];
return this;
};
const once = function (type, listener) {
let once, self;
self = this;
on.call(this, type, once = function () {
off.call(self, type, once);
apply.call(listener, this, arguments);
});
once.__eeOnceListener__ = listener;
return this;
};
const off = function (type, listener) {
let data, listeners, candidate, i;
if (!hasOwnProperty.call(this, '__ee__')) return this;
data = this.__ee__;
if (!data[type]) return this;
listeners = data[type];
if (typeof listeners === 'object') {
for (i = 0; (candidate = listeners[i]); ++i) {
if ((candidate === listener) ||
(candidate.__eeOnceListener__ === listener)) {
if (listeners.length === 2) data[type] = listeners[i ? 0 : 1];
else listeners.splice(i, 1);
}
}
} else {
if ((listeners === listener) ||
(listeners.__eeOnceListener__ === listener)) {
delete data[type];
}
}
return this;
};
const emit = function (type) {
let i, l, listener, listeners, args;
if (!hasOwnProperty.call(this, '__ee__')) return;
listeners = this.__ee__[type];
if (!listeners) return;
if (typeof listeners === 'object') {
l = arguments.length;
args = new Array(l - 1);
for (i = 1; i < l; ++i) args[i - 1] = arguments[i];
listeners = listeners.slice();
for (i = 0; (listener = listeners[i]); ++i) {
apply.call(listener, this, args);
}
} else {
switch (arguments.length) {
case 1:
call.call(listeners, this);
break;
case 2:
call.call(listeners, this, arguments[1]);
break;
case 3:
call.call(listeners, this, arguments[1], arguments[2]);
break;
default:
l = arguments.length;
args = new Array(l - 1);
for (i = 1; i < l; ++i) {
args[i - 1] = arguments[i];
}
apply.call(listeners, this, args);
}
}
};
const descriptors = {
on: {
value: on,
configurable: true,
enumerable: false,
writable: true
},
once: {
value: once,
configurable: true,
enumerable: false,
writable: true
},
off: {
value: off,
configurable: true,
enumerable: false,
writable: true
},
emit: {
value: emit,
configurable: true,
enumerable: false,
writable: true
}
};
const base = defineProperties({}, descriptors);
export default function (o) {
return (o == null) ? create(base) : defineProperties(Object(o), descriptors);
}
export {
on,
once,
off,
emit
}

View file

@ -14,6 +14,7 @@ class Hook {
/**
* Adds a function to be run before a hook completes
* @example this.content.register(function(){...});
* @return {undefined} void
*/
register(){
for(var i = 0; i < arguments.length; ++i) {
@ -28,24 +29,10 @@ class Hook {
}
}
/**
* Removes a function
* @example this.content.deregister(function(){...});
*/
deregister(func){
let hook;
for (let i = 0; i < this.hooks.length; i++) {
hook = this.hooks[i];
if (hook === func) {
this.hooks.splice(i, 1);
break;
}
}
}
/**
* Triggers a hook to run all functions
* @example this.content.trigger(args).then(function(){...});
* @return {Promise} results
*/
trigger(){
var args = arguments;
@ -53,22 +40,41 @@ class Hook {
var promises = [];
this.hooks.forEach(function(task) {
try {
var executing = task.apply(context, args);
} catch (err) {
console.log(err);
}
var executing = task.apply(context, args);
if(executing && typeof executing["then"] === "function") {
// Task is a function that returns a promise
promises.push(executing);
}
// Otherwise Task resolves immediately, continue
// Otherwise Task resolves immediately, add resolved promise with result
promises.push(new Promise((resolve, reject) => {
resolve(executing);
}));
});
return Promise.all(promises);
}
/**
* Triggers a hook to run all functions synchronously
* @example this.content.trigger(args).then(function(){...});
* @return {Array} results
*/
triggerSync(){
var args = arguments;
var context = this.context;
var results = [];
this.hooks.forEach(function(task) {
var executing = task.apply(context, args);
results.push(executing);
});
return results;
}
// Adds a function to be run before a hook completes
list(){

View file

@ -166,4 +166,6 @@ function lookup(filename) {
return filename && mimeTypes[filename.split(".").pop().toLowerCase()] || defaultValue;
};
export default { lookup };
export {
lookup
}

View file

@ -1,102 +0,0 @@
import path from "path-webpack";
/**
* Creates a Path object for parsing and manipulation of a path strings
*
* Uses a polyfill for Nodejs path: https://nodejs.org/api/path.html
* @param {string} pathString a url string (relative or absolute)
* @class
*/
class Path {
constructor(pathString) {
var protocol;
var parsed;
protocol = pathString.indexOf("://");
if (protocol > -1) {
pathString = new URL(pathString).pathname;
}
parsed = this.parse(pathString);
this.path = pathString;
if (this.isDirectory(pathString)) {
this.directory = pathString;
} else {
this.directory = parsed.dir + "/";
}
this.filename = parsed.base;
this.extension = parsed.ext.slice(1);
}
/**
* Parse the path: https://nodejs.org/api/path.html#path_path_parse_path
* @param {string} what
* @returns {object}
*/
parse (what) {
return path.parse(what);
}
/**
* @param {string} what
* @returns {boolean}
*/
isAbsolute (what) {
return path.isAbsolute(what || this.path);
}
/**
* Check if path ends with a directory
* @param {string} what
* @returns {boolean}
*/
isDirectory (what) {
return (what.charAt(what.length-1) === "/");
}
/**
* Resolve a path against the directory of the Path
*
* https://nodejs.org/api/path.html#path_path_resolve_paths
* @param {string} what
* @returns {string} resolved
*/
resolve (what) {
return path.resolve(this.directory, what);
}
/**
* Resolve a path relative to the directory of the Path
*
* https://nodejs.org/api/path.html#path_path_relative_from_to
* @param {string} what
* @returns {string} relative
*/
relative (what) {
var isAbsolute = what && (what.indexOf("://") > -1);
if (isAbsolute) {
return what;
}
return path.relative(this.directory, what);
}
splitPath(filename) {
return this.splitPathRe.exec(filename).slice(1);
}
/**
* Return the path string
* @returns {string} path
*/
toString () {
return this.path;
}
}
export default Path;

View file

@ -1,4 +1,4 @@
import {defer, requestAnimationFrame} from "./core";
import { defer, requestAnimationFrame } from "./core.js";
/**
* Queue for handling tasks one at a time

View file

@ -1,11 +1,10 @@
import { qs, qsa } from "./core";
import Url from "./url";
import Path from "./path";
import { qs } from "./core.js";
import { createUrl, isAbsolute } from "./url.js";
export function replaceBase(doc, section){
var base;
var head;
var url = section.url;
var url = section.href;
var absolute = (url.indexOf("://") > -1);
if(!doc){
@ -21,8 +20,14 @@ export function replaceBase(doc, section){
}
// Fix for Safari crashing if the url doesn't have an origin
if (!absolute && window && window.location) {
url = window.location.origin + url;
if (!absolute && (typeof(window) !== "undefined" && window.location)) {
let parts = window.location.href.split("/")
let directory = "";
parts.pop();
directory = parts.join("/");
url = directory + url;
}
base.setAttribute("href", url);
@ -31,7 +36,7 @@ export function replaceBase(doc, section){
export function replaceCanonical(doc, section){
var head;
var link;
var url = section.canonical;
var url = section.canonical || section.href;
if(!doc){
return;
@ -53,7 +58,7 @@ export function replaceCanonical(doc, section){
export function replaceMeta(doc, section){
var head;
var meta;
var id = section.idref;
var id = section.idref || section.href;
if(!doc){
return;
}
@ -74,15 +79,15 @@ export function replaceMeta(doc, section){
// TODO: move me to Contents
export function replaceLinks(contents, fn) {
var links = contents.querySelectorAll("a[href]");
let links = contents.querySelectorAll("a[href]");
if (!links.length) {
return;
}
var base = qs(contents.ownerDocument, "base");
var location = base ? base.getAttribute("href") : undefined;
var replaceLink = function(link){
let base = qs(contents.ownerDocument, "base");
let location = base ? base.getAttribute("href") : contents.ownerDocument.defaultView.location.href;
let replaceLink = function(link){
var href = link.getAttribute("href");
if(href.indexOf("mailto:") === 0){
@ -91,27 +96,14 @@ export function replaceLinks(contents, fn) {
var absolute = (href.indexOf("://") > -1);
if(absolute){
if(isAbsolute(href)){
link.setAttribute("target", "_blank");
}else{
var linkUrl;
try {
linkUrl = new Url(href, location);
} catch(error) {
// NOOP
}
link.onclick = function(){
if(linkUrl && linkUrl.hash) {
fn(linkUrl.Path.path + linkUrl.hash);
} else if(linkUrl){
fn(linkUrl.Path.path);
} else {
fn(href);
}
let linkUrl = createUrl(href, location);
fn(linkUrl.toString());
return false;
};
@ -128,9 +120,6 @@ export function replaceLinks(contents, fn) {
export function substitute(content, urls, replacements) {
urls.forEach(function(url, i){
if (url && replacements[i]) {
// Account for special characters in the file name.
// See https://stackoverflow.com/a/6318729.
url = url.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
content = content.replace(new RegExp(url, "g"), replacements[i]);
}
});

View file

@ -1,150 +1,76 @@
import {defer, isXml, parse} from "./core";
import Path from "./path";
import {defer, isXml, parse} from "./core.js";
import { extension } from "./url.js";
function request(url, type, withCredentials, headers) {
var supportsURL = (typeof window != "undefined") ? window.URL : false; // TODO: fallback for url if window isn't defined
var BLOB_RESPONSE = supportsURL ? "blob" : "arraybuffer";
var deferred = new defer();
var xhr = new XMLHttpRequest();
//-- Check from PDF.js:
// https://github.com/mozilla/pdf.js/blob/master/web/compatibility.js
var xhrPrototype = XMLHttpRequest.prototype;
var header;
if (!("overrideMimeType" in xhrPrototype)) {
// IE10 might have response, but not overrideMimeType
Object.defineProperty(xhrPrototype, "overrideMimeType", {
value: function xmlHttpRequestOverrideMimeType() {}
});
}
if(withCredentials) {
xhr.withCredentials = true;
}
xhr.onreadystatechange = handler;
xhr.onerror = err;
xhr.open("GET", url, true);
for(header in headers) {
xhr.setRequestHeader(header, headers[header]);
}
if(type == "json") {
xhr.setRequestHeader("Accept", "application/json");
}
function request(url, type, options={}) {
let settings = {
method: 'GET',
headers: options.headers,
credentials: options.credentials
};
// If type isn"t set, determine it from the file extension
if(!type) {
type = new Path(url).extension;
type = extension(url);
}
if(type == "blob"){
xhr.responseType = BLOB_RESPONSE;
}
if(isXml(type)) {
// xhr.responseType = "document";
xhr.overrideMimeType("text/xml"); // for OPF parsing
}
if(type == "xhtml") {
// xhr.responseType = "document";
}
if(type == "html" || type == "htm") {
// xhr.responseType = "document";
}
if(type == "binary") {
xhr.responseType = "arraybuffer";
}
xhr.send();
function err(e) {
deferred.reject(e);
}
function handler() {
if (this.readyState === XMLHttpRequest.DONE) {
var responseXML = false;
if(this.responseType === "" || this.responseType === "document") {
responseXML = this.responseXML;
}
if (this.status === 200 || this.status === 0 || responseXML) { //-- Firefox is reporting 0 for blob urls
var r;
if (!this.response && !responseXML) {
deferred.reject({
status: this.status,
message : "Empty Response",
stack : new Error().stack
});
return deferred.promise;
}
if (this.status === 403) {
deferred.reject({
status: this.status,
response: this.response,
message : "Forbidden",
stack : new Error().stack
});
return deferred.promise;
}
if(responseXML){
r = this.responseXML;
} else
if(isXml(type)){
// xhr.overrideMimeType("text/xml"); // for OPF parsing
// If this.responseXML wasn't set, try to parse using a DOMParser from text
r = parse(this.response, "text/xml");
}else
if(type == "xhtml"){
r = parse(this.response, "application/xhtml+xml");
}else
if(type == "html" || type == "htm"){
r = parse(this.response, "text/html");
}else
if(type == "json"){
r = JSON.parse(this.response);
}else
if(type == "blob"){
if(supportsURL) {
r = this.response;
} else {
//-- Safari doesn't support responseType blob, so create a blob from arraybuffer
r = new Blob([this.response]);
}
}else{
r = this.response;
}
deferred.resolve(r);
} else {
return fetch(url, settings)
.then(function(response) {
let deferred = new defer();
if(response.ok) {
return response;
} else if (response.status === 403) {
deferred.reject({
status: this.status,
message : this.response,
status: this.response.status,
response: this.response.response,
message : "Forbidden",
stack : new Error().stack
});
return deferred.promise;
} else {
deferred.reject({
status: response.status,
message : "Empty Response",
stack : new Error().stack
});
return deferred.promise;
}
}
}
})
.then(function(response) {
if(isXml(type)){
return response.text();
} else if(type == "xhtml"){
return response.text();
} else if(type == "html" || type == "htm"){
return response.text();
} else if(type == "json"){
return response.json();
} else if(type == "blob"){
return response.blob();
} else if(type == "binary"){
return response.arrayBuffer();
} else {
return response.text();
}
})
.then(function(result) {
let r;
return deferred.promise;
if(isXml(type)){
r = parse(result, "text/xml");
} else if(type === "xhtml"){
r = parse(result, "application/xhtml+xml");
} else if(type === "html" || type === "htm"){
r = parse(result, "text/html");
} else if(type === "json"){
r = result;
} else if(type === "blob"){
r = result;
} else {
r = result;
}
return r;
});
}
export default request;

View file

@ -1,108 +1,50 @@
import Path from "./path";
import path from "path-webpack";
export function isAbsolute(inputUrl) {
if (!inputUrl) {
return
}
if (inputUrl instanceof URL) {
return true;
}
return inputUrl.indexOf("://") > -1;
}
/**
* creates a Url object for parsing and manipulation of a url string
* @param {string} urlString a url string (relative or absolute)
* @param {string} [baseString] optional base for the url,
* default to window.location.href
*/
class Url {
constructor(urlString, baseString) {
var absolute = (urlString.indexOf("://") > -1);
var pathname = urlString;
var basePath;
this.Url = undefined;
this.href = urlString;
this.protocol = "";
this.origin = "";
this.hash = "";
this.hash = "";
this.search = "";
this.base = baseString;
if (!absolute &&
baseString !== false &&
typeof(baseString) !== "string" &&
window && window.location) {
this.base = window.location.href;
export function createUrl(inputUrl, base) {
if (inputUrl instanceof URL) {
return inputUrl;
} else if (!base && !isAbsolute(inputUrl)) {
let locationBase = "";
if (typeof(window) !== "undefined" &&
typeof(window.location) !== "undefined") {
locationBase = window.location.href;
}
// URL Polyfill doesn't throw an error if base is empty
if (absolute || this.base) {
try {
if (this.base) { // Safari doesn't like an undefined base
this.Url = new URL(urlString, this.base);
} else {
this.Url = new URL(urlString);
}
this.href = this.Url.href;
this.protocol = this.Url.protocol;
this.origin = this.Url.origin;
this.hash = this.Url.hash;
this.search = this.Url.search;
pathname = this.Url.pathname + (this.Url.search ? this.Url.search : '');
} catch (e) {
// Skip URL parsing
this.Url = undefined;
// resolve the pathname from the base
if (this.base) {
basePath = new Path(this.base);
pathname = basePath.resolve(pathname);
}
}
}
this.Path = new Path(pathname);
this.directory = this.Path.directory;
this.filename = this.Path.filename;
this.extension = this.Path.extension;
}
/**
* @returns {Path}
*/
path () {
return this.Path;
}
/**
* Resolves a relative path to a absolute url
* @param {string} what
* @returns {string} url
*/
resolve (what) {
var isAbsolute = (what.indexOf("://") > -1);
var fullpath;
if (isAbsolute) {
return what;
}
fullpath = path.resolve(this.directory, what);
return this.origin + fullpath;
}
/**
* Resolve a path relative to the url
* @param {string} what
* @returns {string} path
*/
relative (what) {
return path.relative(what, this.directory);
}
/**
* @returns {string}
*/
toString () {
return this.href;
return new URL(inputUrl, locationBase);
} else {
return new URL(inputUrl, base);
}
}
export default Url;
export function filename(inputUrl) {
const url = createUrl(inputUrl);
return url.pathname.split('/').pop();
}
export function directory(inputUrl) {
const url = createUrl(inputUrl);
const name = filename(url);
return url.pathname.replace(name, "");
}
export function extension(inputUrl) {
const name = filename(inputUrl);
return name.split('.').pop();
}
export function resolve(inputUrl, path) {
const url = createUrl(inputUrl);
return new URL(path, url).href;
}
export function relative(inputUrl, path) {
const url = createUrl(inputUrl);
return new URL(path, url).pathname;
}

View file

@ -1,66 +0,0 @@
import Book from '../src/book';
import assert from 'assert';
describe('Book', function() {
describe('Unarchived', function() {
var book = new Book("/fixtures/alice/OPS/package.opf");
it('should open a epub', async function() {
await book.opened
assert.equal(book.isOpen, true, "book is opened");
assert.equal( book.url.toString(), "http://localhost:9876/fixtures/alice/OPS/package.opf", "book url is passed to new Book" );
});
it('should have a local coverUrl', async function() {
assert.equal( await book.coverUrl(), "http://localhost:9876/fixtures/alice/OPS/images/cover_th.jpg", "cover url is available" );
});
});
describe('Archived epub', function() {
var book = new Book("/fixtures/alice.epub");
it('should open a archived epub', async function() {
await book.opened
assert.equal(book.isOpen, true, "book is opened");
assert(book.archive, "book is unarchived");
});
it('should have a blob coverUrl', async function() {
let coverUrl = await book.coverUrl()
assert( /^blob:http:\/\/localhost:9876\/[^\/]+$/.test(coverUrl), "cover url is available and a blob: url" );
});
});
describe('Archived epub in array buffer without options', function() {
let book;
before(async function() {
const response = await fetch("/fixtures/alice.epub");
const buffer = await response.arrayBuffer()
book = new Book(buffer)
})
it('should open a archived epub', async function() {
await book.opened
assert.equal(book.isOpen, true, "book is opened");
assert(book.archive, "book is unarchived");
});
it('should have a blob coverUrl', async function() {
let coverUrl = await book.coverUrl()
assert( /^blob:http:\/\/localhost:9876\/[^\/]+$/.test(coverUrl), "cover url is available and a blob: url" );
});
});
describe('Archived epub without cover', function() {
var book = new Book("/fixtures/alice_without_cover.epub");
it('should open a archived epub', async function() {
await book.opened
assert.equal(book.isOpen, true, "book is opened");
assert(book.archive, "book is unarchived");
});
it('should have a empty coverUrl', async function() {
let coverUrl = await book.coverUrl()
assert.equal(coverUrl, null, "cover url should be null" );
});
});
});

View file

@ -1,199 +0,0 @@
import assert from 'assert';
import Url from '../src/utils/url';
import Path from '../src/utils/path';
describe('Core', function() {
before(function(){
});
describe('Url', function () {
it("Url()", function() {
var url = new Url("http://example.com/fred/chasen/derf.html");
assert.equal( url.href, "http://example.com/fred/chasen/derf.html" );
assert.equal( url.directory, "/fred/chasen/" );
assert.equal( url.extension, "html" );
assert.equal( url.filename, "derf.html" );
assert.equal( url.origin, "http://example.com" );
assert.equal( url.protocol, "http:" );
assert.equal( url.search, "" );
});
describe('#resolve()', function () {
it("should join subfolders", function() {
var a = "http://example.com/fred/chasen/";
var b = "ops/derf.html";
var resolved = new Url(a).resolve(b);
assert.equal( resolved, "http://example.com/fred/chasen/ops/derf.html" );
});
it("should resolve up a level", function() {
var a = "http://example.com/fred/chasen/index.html";
var b = "../derf.html";
var resolved = new Url(a).resolve(b);
assert.equal( resolved, "http://example.com/fred/derf.html" );
});
it("should resolve absolute", function() {
var a = "http://example.com/fred/chasen/index.html";
var b = "/derf.html";
var resolved = new Url(a).resolve(b);
assert.equal( resolved, "http://example.com/derf.html" );
});
it("should resolve with search strings", function() {
var a = "http://example.com/fred/chasen/index.html?debug=true";
var b = "/derf.html";
var resolved = new Url(a).resolve(b);
assert.equal( resolved, "http://example.com/derf.html" );
});
// Doesn't work with path.parse
xit("should handle directory with a dot", function() {
var a = "http://example.com/fred/chasen/index.epub/";
var url = new Url(a);
assert.equal( url.directory, "/fred/chasen/index.epub/" );
assert.equal( url.extension, "" );
});
it("should handle file urls", function() {
var url = new Url("file:///var/mobile/Containers/Data/Application/F47E4434-9B98-4654-93F1-702336B08EE6/Documents/books/moby-dick/derf.html");
assert.equal( url.href, "file:///var/mobile/Containers/Data/Application/F47E4434-9B98-4654-93F1-702336B08EE6/Documents/books/moby-dick/derf.html" );
assert.equal( url.directory, "/var/mobile/Containers/Data/Application/F47E4434-9B98-4654-93F1-702336B08EE6/Documents/books/moby-dick/" );
assert.equal( url.extension, "html" );
assert.equal( url.filename, "derf.html" );
assert.equal( url.origin, "file://" ); // origin should be blank
assert.equal( url.protocol, "file:" );
assert.equal( url.search, "" );
});
it("should resolve with file urls", function() {
var a = "file:///var/mobile/Containers/Data/Application/books/";
var b = "derf.html";
var resolved = new Url(a).resolve(b);
assert.equal( resolved, "file:///var/mobile/Containers/Data/Application/books/derf.html" );
});
});
});
describe('Path', function () {
it("Path()", function() {
var path = new Path("/fred/chasen/derf.html");
assert.equal( path.path, "/fred/chasen/derf.html" );
assert.equal( path.directory, "/fred/chasen/" );
assert.equal( path.extension, "html" );
assert.equal( path.filename, "derf.html" );
});
it("Strip out url", function() {
var path = new Path("http://example.com/fred/chasen/derf.html");
assert.equal( path.path, "/fred/chasen/derf.html" );
assert.equal( path.directory, "/fred/chasen/" );
assert.equal( path.extension, "html" );
assert.equal( path.filename, "derf.html" );
});
describe('#parse()', function () {
it("should parse a path", function() {
var path = Path.prototype.parse("/fred/chasen/derf.html");
assert.equal( path.dir, "/fred/chasen" );
assert.equal( path.base, "derf.html" );
assert.equal( path.ext, ".html" );
});
it("should parse a relative path", function() {
var path = Path.prototype.parse("fred/chasen/derf.html");
assert.equal( path.dir, "fred/chasen" );
assert.equal( path.base, "derf.html" );
assert.equal( path.ext, ".html" );
});
});
describe('#isDirectory()', function () {
it("should recognize a directory", function() {
var directory = Path.prototype.isDirectory("/fred/chasen/");
var notDirectory = Path.prototype.isDirectory("/fred/chasen/derf.html");
assert(directory, "/fred/chasen/ is a directory" );
assert(!notDirectory, "/fred/chasen/derf.html is not directory" );
});
});
describe('#resolve()', function () {
it("should resolve a path", function() {
var a = "/fred/chasen/index.html";
var b = "derf.html";
var resolved = new Path(a).resolve(b);
assert.equal(resolved, "/fred/chasen/derf.html" );
});
it("should resolve a relative path", function() {
var a = "fred/chasen/index.html";
var b = "derf.html";
var resolved = new Path(a).resolve(b);
assert.equal(resolved, "/fred/chasen/derf.html" );
});
it("should resolve a level up", function() {
var a = "/fred/chasen/index.html";
var b = "../derf.html";
var resolved = new Path(a).resolve(b);
assert.equal(resolved, "/fred/derf.html" );
});
});
describe('#relative()', function () {
it("should find a relative path at the same level", function() {
var a = "/fred/chasen/index.html";
var b = "/fred/chasen/derf.html";
var relative = new Path(a).relative(b);
assert.equal(relative, "derf.html" );
});
it("should find a relative path down a level", function() {
var a = "/fred/chasen/index.html";
var b = "/fred/chasen/ops/derf.html";
var relative = new Path(a).relative(b);
assert.equal(relative, "ops/derf.html" );
});
it("should resolve a level up", function() {
var a = "/fred/chasen/index.html";
var b = "/fred/derf.html";
var relative = new Path(a).relative(b);
assert.equal(relative, "../derf.html" );
});
});
});
});

View file

@ -1,52 +0,0 @@
import assert from 'assert';
import ePub from '../src/epub';
// var sinon = require('sinon');
describe('ePub', function() {
var server;
before(function(){
/*
// var packageContents = fs.readFileSync(__dirname + '/../books/moby-dick/OPS/package.opf', 'utf8');
// var tocContents = fs.readFileSync(__dirname + '/../books/moby-dick/OPS/toc.xhtml', 'utf8');
var packageContents = require('./fixtures/moby-dick/OPS/package.opf');
var tocContents = require('./fixtures/moby-dick/OPS/toc.xhtml');
server = sinon.fakeServer.create();
server.autoRespond = true;
server.respondWith("moby-dick/OPS/package.opf", [200, {
"Content-Type": "text/xml"
}, packageContents]);
server.respondWith("moby-dick/OPS/toc.xhtml", [200, {
"Content-Type": "application/xhtml+xml"
}, tocContents]);
*/
});
after(function(){
// server.restore();
});
it('should open a epub', function() {
var book = ePub("/fixtures/alice/OPS/package.opf");
return book.opened.then(function(){
assert.equal( book.isOpen, true, "book is opened" );
assert.equal( book.url.toString(), "http://localhost:9876/fixtures/alice/OPS/package.opf", "book url is passed to new Book" );
});
});
it('should open a archived epub', function() {
var book = ePub("/fixtures/alice.epub");
// assert(typeof (JSZip) !== "undefined", "JSZip is present" );
return book.opened.then(function(){
assert.equal( book.isOpen, true, "book is opened" );
assert( book.archive, "book is unarchived" );
});
});
});

View file

@ -1,425 +0,0 @@
import assert from 'assert';
import EpubCFI from '../src/epubcfi.js';
// var fs = require('fs');
if (typeof DOMParser === "undefined") {
global.DOMParser = require('xmldom').DOMParser;
}
describe('EpubCFI', function() {
it('parse a cfi on init', function() {
var cfi = new EpubCFI("epubcfi(/6/2[cover]!/6)");
assert.equal( cfi.spinePos, 0, "spinePos is parsed as the first item" );
});
it('parse a cfi and ignore the base if present', function() {
var cfi = new EpubCFI("epubcfi(/6/2[cover]!/6)", "/6/6[end]");
assert.equal( cfi.spinePos, 0, "base is ignored and spinePos is parsed as the first item" );
});
describe('#parse()', function() {
var cfi = new EpubCFI();
it('parse a cfi on init', function() {
var parsed = cfi.parse("epubcfi(/6/2[cover]!/6)");
assert.equal( parsed.spinePos, 0, "spinePos is parsed as the first item" );
});
it('parse a cfi and ignore the base if present', function() {
var parsed = cfi.parse("epubcfi(/6/2[cover]!/6)", "/6/6[end]");
assert.equal( parsed.spinePos, 0, "base is ignored and spinePos is parsed as the first item" );
});
it('parse a cfi with a character offset', function() {
var parsed = cfi.parse("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)");
assert.equal( parsed.path.terminal.offset, 3, "Path has a terminal offset of 3" );
});
it('parse a cfi with a range', function() {
var parsed = cfi.parse("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4)");
assert.equal( parsed.range, true, "Range is true" );
assert.equal( parsed.start.steps.length, 2, "Start steps are present" );
assert.equal( parsed.end.steps.length, 1, "End steps are present" );
assert.equal( parsed.start.terminal.offset, 1, "Start has a terminal offset of 1" );
assert.equal( parsed.end.terminal.offset, 4, "End has a terminal offset of 4" );
});
});
describe('#toString()', function() {
it('parse a cfi and write it back', function() {
assert.equal(new EpubCFI("epubcfi(/6/2[cover]!/6)").toString(), "epubcfi(/6/2[cover]!/6)", "output cfi string is same as input" );
assert.equal(new EpubCFI("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)").toString(), "epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)", "output cfi string is same as input" );
assert.equal(new EpubCFI("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4)").toString(), "epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4)", "output cfi string is same as input" );
});
});
describe('#checkType()', function() {
it('determine the type of a cfi string', function() {
var cfi = new EpubCFI();
assert.equal( cfi.checkType('epubcfi(/6/2[cover]!/6)'), 'string' );
assert.equal( cfi.checkType('/6/2[cover]!/6'), false );
});
it('determine the type of a cfi', function() {
var ogcfi = new EpubCFI("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)");
var cfi = new EpubCFI();
assert.equal( cfi.checkType(ogcfi), 'EpubCFI' );
});
it('determine the type of a node', function() {
var cfi = new EpubCFI();
var el = document.createElement('div');
assert.equal( cfi.checkType(el), 'node' );
});
it('determine the type of a range', function() {
var cfi = new EpubCFI();
var range = document.createRange();
assert.equal( cfi.checkType(range), 'range' );
});
});
describe('#compare()', function() {
it('compare CFIs', function() {
var epubcfi = new EpubCFI();
// Spines
assert.equal(epubcfi.compare("epubcfi(/6/4[cover]!/4)", "epubcfi(/6/2[cover]!/4)"), 1, "First spine is greater");
assert.equal(epubcfi.compare("epubcfi(/6/4[cover]!/4)", "epubcfi(/6/6[cover]!/4)"), -1, "Second spine is greater");
// First is deeper
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/8/2)", "epubcfi(/6/2[cover]!/6)"), 1, "First Element is after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/2)", "epubcfi(/6/2[cover]!/6)"), -1, "First Element is before Second");
// Second is deeper
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/8/2)", "epubcfi(/6/2[cover]!/6/4/2/2)"), 1, "First Element is after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/4)", "epubcfi(/6/2[cover]!/6/4/2/2)"), -1, "First Element is before Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/6)", "epubcfi(/6/2[cover]!/4/6/8/1:0)"), -1, "First is less specific, so is before Second");
// Same Depth
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/6/8)", "epubcfi(/6/2[cover]!/6/2)"), 1, "First Element is after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/20)", "epubcfi(/6/2[cover]!/6/10)"), -1, "First Element is before Second");
// Text nodes
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/5)", "epubcfi(/6/2[cover]!/4/3)"), 1, "First TextNode is after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/7)", "epubcfi(/6/2[cover]!/4/13)"), -1, "First TextNode is before Second");
// Char offset
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/5:1)", "epubcfi(/6/2[cover]!/4/5:0)"), 1, "First Char Offset after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/5:2)", "epubcfi(/6/2[cover]!/4/5:30)"), -1, "Second Char Offset before Second");
// Normal example
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/8/5:1)", "epubcfi(/6/2[cover]!/4/6/15:2)"), 1, "First Element after Second");
assert.equal(epubcfi.compare("epubcfi(/6/2[cover]!/4/8/1:0)", "epubcfi(/6/2[cover]!/4/8/1:0)"), 0, "All Equal");
// Different Lengths
assert.equal(epubcfi.compare(
'epubcfi(/6/16[id42]!/4[5N3C0-8c483216e03a4ff49927fc1a97dc7b2c]/10/1:317)',
'epubcfi(/6/16[id42]!/4[5N3C0-8c483216e03a4ff49927fc1a97dc7b2c]/10/2[page18]/1:0)'
), -1, "First CFI is before Second");
assert.equal(epubcfi.compare(
'epubcfi(/6/16[id42]!/4[5N3C0-8c483216e03a4ff49927fc1a97dc7b2c]/12/1:0)',
'epubcfi(/6/16[id42]!/4[5N3C0-8c483216e03a4ff49927fc1a97dc7b2c]/12/2/1:9)'
), -1, "First CFI is before Second");
assert.equal(epubcfi.compare(
'epubcfi(/6/16!/4/12/1:0)',
'epubcfi(/6/16!/4/12/2/1:9)'
), -1, "First CFI is before Second");
});
});
describe('#fromNode()', function() {
var base = "/6/4[chap01ref]";
// var contents = fs.readFileSync(__dirname + '/fixtures/chapter1-highlights.xhtml', 'utf8');
var contents = require('./fixtures/chapter1-highlights.xhtml').default;
// var serializer = new XMLSerializer();
// var doc = serializer.serializeToString(contents);
var doc = new DOMParser().parseFromString(contents, "application/xhtml+xml");
it('get a cfi from a p node', function() {
var span = doc.getElementById('c001p0004');
var cfi = new EpubCFI(span, base);
assert.equal(span.nodeType, Node.ELEMENT_NODE, "provided a element node");
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/10/2[c001p0004])" );
});
it('get a cfi from a text node', function() {
var t = doc.getElementById('c001p0004').childNodes[0];
var cfi = new EpubCFI(t, base);
assert.equal(t.nodeType, Node.TEXT_NODE, "provided a text node");
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/10/2[c001p0004]/1)" );
});
it('get a cfi from a text node inside a highlight', function() {
var t = doc.getElementById('highlight-1').childNodes[0];
var cfi = new EpubCFI(t, base, 'annotator-hl');
assert.equal(t.nodeType, Node.TEXT_NODE, "provided a text node");
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/32/2[c001p0017]/1)" );
});
it('get a cfi from a highlight node', function() {
var t = doc.getElementById('highlight-1');
var cfi = new EpubCFI(t, base, 'annotator-hl');
assert.equal(t.nodeType, Node.ELEMENT_NODE, "provided a highlight node");
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/32/2[c001p0017])" );
});
});
describe('#fromRange()', function() {
var base = "/6/4[chap01ref]";
// var contentsClean = fs.readFileSync(__dirname + '/fixtures/chapter1.xhtml', 'utf8');
var contentsClean = require('./fixtures/chapter1.xhtml').default;
var doc = new DOMParser().parseFromString(contentsClean, "application/xhtml+xml");
// var contentsHighlights = fs.readFileSync(__dirname + '/fixtures/chapter1-highlights.xhtml', 'utf8');
var contentsHighlights = require('./fixtures/chapter1-highlights.xhtml').default;
var docHighlights = new DOMParser().parseFromString(contentsHighlights, "application/xhtml+xml");
// var highlightContents = fs.readFileSync(__dirname + '/fixtures/highlight.xhtml', 'utf8');
var highlightContents = require('./fixtures/highlight.xhtml').default;
var docHighlightsAlice = new DOMParser().parseFromString(highlightContents, "application/xhtml+xml");
it('get a cfi from a collapsed range', function() {
var t1 = doc.getElementById('c001p0004').childNodes[0];
var t2 = doc.getElementById('c001p0007').childNodes[0];
var range = doc.createRange();
var cfi;
range.setStart(t1, 6);
cfi = new EpubCFI(range, base);
assert.equal( cfi.range, false);
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/10/2[c001p0004]/1:6)" );
});
it('get a cfi from a range', function() {
var t1 = doc.getElementById('c001p0004').childNodes[0];
var t2 = doc.getElementById('c001p0007').childNodes[0];
var range = doc.createRange();
var cfi;
range.setStart(t1, 6);
range.setEnd(t2, 27);
cfi = new EpubCFI(range, base);
assert.equal( cfi.range, true);
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2,/10/2[c001p0004]/1:6,/16/2[c001p0007]/1:27)" );
});
it('get a cfi from a range with offset 0', function() {
var t1 = doc.getElementById('c001p0004').childNodes[0];
var range = doc.createRange();
var cfi;
range.setStart(t1, 0);
range.setEnd(t1, 1);
cfi = new EpubCFI(range, base);
assert.equal( cfi.range, true);
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/10/2[c001p0004],/1:0,/1:1)" );
});
it('get a cfi from a range inside a highlight', function() {
var t1 = docHighlights.getElementById('highlight-1').childNodes[0];
var range = docHighlights.createRange();
var cfi;
range.setStart(t1, 6);
cfi = new EpubCFI(range, base, 'annotator-hl');
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/32/2[c001p0017]/1:43)" );
});
// TODO: might need to have double ranges in front
it('get a cfi from a range past a highlight', function() {
var t1 = docHighlights.getElementById('c001s0001').childNodes[1];
var range = docHighlights.createRange();
var cfi;
range.setStart(t1, 25);
cfi = new EpubCFI(range, base, 'annotator-hl');
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/4/2[c001s0001]/1:41)" );
});
it('get a cfi from a range in between two highlights', function() {
var t1 = docHighlightsAlice.getElementById('p2').childNodes[1];
var range = docHighlightsAlice.createRange();
var cfi;
range.setStart(t1, 4);
cfi = new EpubCFI(range, base, 'annotator-hl');
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/4[p2]/1:123)" );
});
it('correctly count text nodes, independent of any elements present inbetween', function() {
var t1 = docHighlightsAlice.getElementById('p3').childNodes[2];
var range = docHighlightsAlice.createRange();
var cfi;
range.setStart(t1, 4);
cfi = new EpubCFI(range, base);
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/6[p3]/3:4)" );
});
});
describe('#toRange()', function() {
var base = "/6/4[chap01ref]";
// var contents = fs.readFileSync(__dirname + '/fixtures/chapter1-highlights.xhtml', 'utf8');
var contents = require('./fixtures/chapter1-highlights.xhtml').default;
var doc = new DOMParser().parseFromString(contents, "application/xhtml+xml");
// var serializer = new XMLSerializer();
// console.log(serializer.serializeToString(doc));
it('get a range from a cfi', function() {
var t1 = doc.getElementById('c001p0004').childNodes[0];
var t2 = doc.getElementById('c001p0007').childNodes[0];
var ogRange = doc.createRange();
var cfi;
var newRange;
ogRange.setStart(t1, 6);
cfi = new EpubCFI(ogRange, base);
// Check it was parse correctly
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/10/2[c001p0004]/1:6)" );
// Check the range
newRange = cfi.toRange(doc);
assert.equal( newRange.startContainer, t1);
assert.equal( newRange.startOffset, 6);
assert.equal( newRange.collapsed, true);
});
it('get a range from a cfi with a range', function() {
var t1 = doc.getElementById('c001p0004').childNodes[0];
var t2 = doc.getElementById('c001p0007').childNodes[0];
var ogRange = doc.createRange();
var cfi;
var newRange;
ogRange.setStart(t1, 6);
ogRange.setEnd(t2, 27);
cfi = new EpubCFI(ogRange, base);
// Check it was parse correctly
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2,/10/2[c001p0004]/1:6,/16/2[c001p0007]/1:27)" );
// Check the range
newRange = cfi.toRange(doc);
assert.equal( newRange.startContainer, t1);
assert.equal( newRange.startOffset, 6);
assert.equal( newRange.endContainer, t2);
assert.equal( newRange.endOffset, 27);
assert.equal( newRange.collapsed, false);
});
it('get a cfi from a range inside a highlight', function() {
var t1 = doc.getElementById('highlight-1').childNodes[0];
var ogRange = doc.createRange();
var cfi;
var newRange;
ogRange.setStart(t1, 6);
cfi = new EpubCFI(ogRange, base, 'annotator-hl');
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/32/2[c001p0017]/1:43)" );
// Check the range
newRange = cfi.toRange(doc, 'annotator-hl');
assert.ok(newRange.startContainer);
assert.equal( newRange.startContainer, t1);
assert.equal( newRange.startOffset, 6);
});
it('get a cfi from a range inside a highlight range', function() {
var t1 = doc.getElementById('highlight-2').childNodes[0];
var t2 = doc.getElementById('c001s0001').childNodes[1];
var ogRange = doc.createRange();
var cfi;
var newRange;
ogRange.setStart(t1, 5);
ogRange.setEnd(t2, 25);
cfi = new EpubCFI(ogRange, base, 'annotator-hl');
assert.equal( cfi.toString(), "epubcfi(/6/4[chap01ref]!/4/2/4/2[c001s0001],/1:5,/1:41)" );
// Check the range
newRange = cfi.toRange(doc, 'annotator-hl');
assert.strictEqual( newRange.startContainer.textContent, t1.textContent);
// assert.strictEqual( newRange.startContainer, t1);
// assert.equal( newRange.startOffset, 5);
});
});
});

Binary file not shown.

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
<rootfiles>
<rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>

View file

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00004"><a id="I_DOWN_THE_RABBIT-HOLE"></a>Down The Rabbit-Hole</h2>
<figure class="small">
<img src= "images/i001_th.jpg" alt="Illo1" />
</figure>
<p>Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do. Once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, "and what is the use of a book," thought Alice, "without pictures or conversations?"</p>
<p>So she was considering in her own mind (as well as she could, for the day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.</p>
<figure class="small">
<img src="images/i002_th.jpg" alt="Illo2" />
</figure>
<p>There was nothing so very remarkable in that, nor did Alice think it so
<a id="Page_4" class="pageno" title="[Pg 4]"></a>very much out of the way to hear the Rabbit
say to itself, "Oh dear! Oh dear! I shall be too late!" But when the Rabbit actually
took a watch out of its waistcoat-pocket and looked at it and then hurried on, Alice
started to her feet, for it flashed across her mind that she had never before seen a
rabbit with either a waistcoat-pocket, or a watch to take out of it, and, burning with
curiosity, she ran across the field after it and was just in time to see it pop down a
large rabbit-hole, under the hedge. In another moment, down went Alice after it!</p>
<p>The rabbit-hole went straight on like a tunnel for some way and then dipped suddenly
down, so suddenly that Alice had not a moment to think about stopping herself before
she found herself falling down what seemed to be a very deep well.</p>
<p>Either the well was very deep, or she fell very slowly, for she had plenty of time,
as she went down, to look about her. First, she tried to make out what she was coming
to, but it was too dark to see anything; then she looked at the sides of the well and
noticed that they were filled with cupboards and book-shelves; here and there she saw
maps and <a id="Page_5" class="pageno" title="[Pg 5]"></a>pictures hung upon pegs. She
took down a jar from one of the shelves as she passed. It was labeled "ORANGE
MARMALADE," but, to her great disappointment, it was empty; she did not like to drop
the jar, so managed to put it into one of the cupboards as she fell past it.</p>
<p>Down, down, down! Would the fall never come to an end? There was nothing else to do,
so Alice soon began talking to herself. "Dinah'll miss me very much to-night, I should
think!" (Dinah was the cat.) "I hope they'll remember her saucer of milk at tea-time.
Dinah, my dear, I wish you were down here with me!" Alice felt that she was dozing off,
when suddenly, thump! thump! down she came upon a heap of sticks and dry leaves, and
the fall was over.</p>
<p>Alice was not a bit hurt, and she jumped up in a moment. She looked up, but it was
all dark overhead; before her was another long passage and the White Rabbit was still
in sight, hurrying down it. There was not a moment to be lost. Away went Alice like the
wind and was just in time to hear it say, as it turned a corner, "Oh, my ears and
whiskers, how late it's getting!" She was close behind it when she turned the corner,
but the Rabbit was no longer to be seen.</p>
<p>She found herself in a long, low hall, which was lit up by a row of lamps hanging
from the roof. There were doors all 'round the hall, but they were all locked; and when
Alice had been all the way down one side and up the other, trying every door, she
walked sadly down the middle, <a id="Page_6" class="pageno" title=
"[Pg 6]"></a>wondering how she was ever to get out again.</p>
<figure class="small">
<img src="images/i003_th.jpg" alt="Illo3" />
</figure>
<p>Suddenly she came upon a little table, all made of solid glass. There was nothing on
it but a tiny golden key, and Alice's first idea was that this might belong to one of
the doors of the hall; but, alas! either the locks were too large, or the key was too
small, but, at any rate, it would not open any of them. However, on the second time
'round, she came upon a low curtain she had not noticed before, and behind it was a
little door about fifteen inches high. She tried the little golden key in the lock, and
to her great delight, it fitted!</p>
<p>Alice opened the door and found that it led into a small passage, not much larger
than a rat-hole; she knelt down and looked along the passage into the loveliest garden
you ever saw. How she longed to get out of that dark hall and wander about among those
beds of bright flowers and those cool fountains, but she could not even get her head
through the doorway. "Oh," said Alice, "how I wish I could shut up like a telescope! I
think I could, if I only knew how to begin."</p>
<p>Alice went back to the table, half hoping she might find another key on it, or at
any rate, a book of rules for shutting people up like telescopes. This time she<a id=
"Page_7" class="pageno" title="[Pg 7]"></a> found a little bottle on it ("which
certainly was not here before," said Alice), and tied 'round the neck of the bottle was
a paper label, with the words "DRINK ME" beautifully printed on it in large
letters.</p>
<p>"No, I'll look first," she said, "and see whether it's marked '<i>poison</i>' or
not," for she had never forgotten that, if you drink from a bottle marked "poison," it
is almost certain to disagree with you, sooner or later. However, this bottle was
<i>not</i> marked "poison," so Alice ventured to taste it, and, finding it very nice
(it had a sort of mixed flavor of cherry-tart, custard, pineapple, roast turkey, toffy
and hot buttered toast), she very soon finished it off.</p>
<p>"What a curious feeling!" said Alice. "I must be shutting up like a telescope!"</p>
<p>And so it was indeed! She was now only ten inches high, and her face brightened up
at the thought that she was now the right size for going through the little door into
that lovely garden.</p>
<p>After awhile, finding that nothing more happened, she decided on going into the
garden at once; but, alas for poor Alice! When she got to the door, she found she had
forgotten the little golden key, and when she went back to the table for it, she found
she could not possibly reach it: she could see it quite plainly through the glass and
she tried her best to climb up one of the legs of the table, but it was too slippery,
and when she had tired herself out with trying, the poor little thing sat down and
cried.</p>
<p>"Come, there's no use in crying like that!" said Alice to herself rather sharply. "I
advise you to<a id="Page_8" class="pageno" title="[Pg 8]"></a> leave off this minute!"
She generally gave herself very good advice (though she very seldom followed it), and
sometimes she scolded herself so severely as to bring tears into her eyes.</p>
<p>Soon her eye fell on a little glass box that was lying under the table: she opened
it and found in it a very small cake, on which the words "EAT ME" were beautifully
marked in currants. "Well, I'll eat it," said Alice, "and if it makes me grow larger, I
can reach the key; and if it makes me grow smaller, I can creep under the door: so
either way I'll get into the garden, and I don't care which happens!"</p>
<p>She ate a little bit and said anxiously to herself, "Which way? Which way?" holding
her hand on the top of her head to feel which way she was growing; and she was quite
surprised to find that she remained the same size. So she set to work and very soon
finished off the cake.</p>
<figure class="small">
<img src="images/i004_th.jpg" alt="Illo4" />
</figure>
<p><a id="Page_9" class="pageno" title="[Pg 9]"></a></p>
</section>
</body>
</html>

View file

@ -1,141 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00005"><a id="II_THE_POOL_OF_TEARS"></a>The Pool Of Tears</h2>
<p>Curiouser and curiouser!" cried Alice (she was so much
surprised that for the moment she quite forgot how to speak good English). "Now I'm
opening out like the largest telescope that ever was! Good-by, feet! Oh, my poor little
feet, I wonder who will put on your shoes and stockings for you now, dears? I shall be
a great deal too far off to trouble myself about you."</p>
<p>Just at this moment her head struck against the roof of the hall; in fact, she was
now rather more than nine feet high, and she at once took up the little golden key and
hurried off to the garden door.</p>
<p>Poor Alice! It was as much as she could do, lying down on one side, to look through
into the garden with one eye; but to get through was more hopeless than ever. She sat
down and began to cry again.</p>
<p>She went on shedding gallons of tears, until there was a large pool all 'round her
and reaching half down the hall.</p>
<p>After a time, she heard a little pattering of feet in the distance and she hastily
dried her eyes to see what was coming. It was the White Rabbit returning, splendidly
dressed, with a pair of white kid-gloves in one hand and a large fan in the other.
He<a id="Page_10" class="pageno" title="[Pg 10]"></a> came trotting along in a great
hurry, muttering to himself, "Oh! the Duchess, the Duchess! Oh! <i>won't</i> she be
savage if I've kept her waiting!"</p>
<figure class="small">
<img src="images/i005_th.jpg" alt="Illo5" />
</figure>
<p>When the Rabbit came near her, Alice began, in a low, timid voice, "If you please,
sir&#8212;" The Rabbit started violently, dropped the white kid-gloves and the fan and
skurried away into the darkness as hard as he could go.</p>
<p>Alice took up the fan and gloves and she kept fanning herself all the time she went
on talking. "Dear, dear! How queer everything is to-day! And yesterday things went on
just as usual. <i>Was</i> I the same when I got up this morning? But if I'm not the
same, the next question is, 'Who in the world am I?' Ah, <i>that's</i> the great
puzzle!"</p>
<p>As she said this, she looked down at her hands and was surprised to see that she had
put on one of the Rabbit's little white kid-gloves while she was talking. "How
<i>can</i> I have done that?" she thought. "I must be growing small again." She got up
and went to the table to measure herself by it and found that she was now about two
feet high and was going on<a id="Page_11" class="pageno" title="[Pg 11]"></a> shrinking
rapidly. She soon found out that the cause of this was the fan she was holding and she
dropped it hastily, just in time to save herself from shrinking away altogether.</p>
<p>"That <i>was</i> a narrow escape!" said Alice, a good deal frightened at the sudden
change, but very glad to find herself still in existence. "And now for the garden!" And
she ran with all speed back to the little door; but, alas! the little door was shut
again and the little golden key was lying on the glass table as before. "Things are
worse than ever," thought the poor child, "for I never was so small as this before,
never!"</p>
<p>As she said these words, her foot slipped, and in another moment, splash! she was up
to her chin in salt-water. Her first idea was that she had somehow fallen into the sea.
However, she soon made out that she was in the pool of tears which she had wept when
she was nine feet high.</p>
<figure class="small">
<img src="images/i006_th.jpg" alt="Illo6" />
</figure>
<p>Just then she heard something splashing about in the pool a little way off, and she
swam nearer to see what it was: she soon made out that it was only a mouse that had
slipped in like herself.<a id="Page_12" class="pageno" title="[Pg 12]"></a></p>
<p>"Would it be of any use, now," thought Alice, "to speak to this mouse? Everything is
so out-of-the-way down here that I should think very likely it can talk; at any rate,
there's no harm in trying." So she began, "O Mouse, do you know the way out of this
pool? I am very tired of swimming about here, O Mouse!" The Mouse looked at her rather
inquisitively and seemed to her to wink with one of its little eyes, but it said
nothing.</p>
<p>"Perhaps it doesn't understand English," thought Alice. "I dare say it's a French
mouse, come over with William the Conqueror." So she began again: "O&#249; est ma
chatte?" which was the first sentence in her French lesson-book. The Mouse gave a
sudden leap out of the water and seemed to quiver all over with fright. "Oh, I beg your
pardon!" cried Alice hastily, afraid that she had hurt the poor animal's feelings. "I
quite forgot you didn't like cats."</p>
<p>"Not like cats!" cried the Mouse in a shrill, passionate voice. "Would <i>you</i>
like cats, if you were me?"</p>
<p>"Well, perhaps not," said Alice in a soothing tone; "don't be angry about it. And
yet I wish I could show you our cat Dinah. I think you'd take a fancy to cats, if you
could only see her. She is such a dear, quiet thing." The Mouse was bristling all over
and she felt certain it must be really offended. "We won't talk about her any more, if
you'd rather not."</p>
<p>"We, indeed!" cried the Mouse, who was trembling down to the end of its tail. "As if
<i>I</i> would talk on such a subject! Our family always <i>hated</i> cats<a id=
"Page_13" class="pageno" title="[Pg 13]"></a>&#8212;nasty, low, vulgar things! Don't
let me hear the name again!"</p>
<figure class="full">
<img src= "images/plate02_th.jpg" alt="Alice at the Mad Tea Party." title="Alice at the Mad Tea Party." />
<figcaption>
<p>Alice at the Mad Tea Party.</p>
</figcaption>
</figure>
<p>"I won't indeed!" said Alice, in a great hurry to change the subject of
conversation. "Are you&#8212;are you fond&#8212;of&#8212;of dogs? There is such a nice
little dog near our house, I should like to show you! It kills all the rats
and&#8212;oh, dear!" cried Alice in a sorrowful tone. "I'm afraid I've offended it
again!" For the Mouse was swimming away from her as hard as it could go, and making
quite a commotion in the pool as it went.</p>
<p>So she called softly after it, "Mouse dear! Do come back again, and we won't talk
about cats, or dogs either, if you don't like them!" When the Mouse heard this, it
turned 'round and swam slowly back to her; its face was quite pale, and it said, in a
low, trembling voice, "Let us get to the shore and then I'll tell you my history and
you'll understand why it is I hate cats and dogs."</p>
<p>It was high time to go, for the pool was getting quite crowded with the birds and
animals that had fallen into it; there were a Duck and a Dodo, a Lory and an Eaglet,
and several other curious creatures. Alice led the way and the whole party swam to the
shore.</p>
<figure class="small">
<img src="images/i007_th.jpg" alt="Illo7" />
</figure>
<p><a id="Page_14" class="pageno" title="[Pg 14]"></a></p>
</section>
</body>
</html>

View file

@ -1,177 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00006">
<a id="III_A_CAUCUS-RACE_AND_A_LONG_TALE"></a>
A Caucus-Race And A Long Tale
</h2>
<p>They were indeed a queer-looking party that assembled on the
bank&#8212;the birds with draggled feathers, the animals with their fur clinging close
to them, and all dripping wet, cross and uncomfortable.</p>
<figure class="small">
<img src="images/i008_th.jpg" alt="Illo8" />
</figure>
<p>The first question, of course, was how to get dry again. They had a consultation
about this and after a few minutes, it seemed quite natural to Alice to find herself
talking familiarly with them, as if she had known them all her life.</p>
<p>At last the Mouse, who seemed to be a person of<a id="Page_15" class="pageno" title=
"[Pg 15]"></a> some authority among them, called out, "Sit down, all of you, and listen
to me! <i>I'll</i> soon make you dry enough!" They all sat down at once, in a large
ring, with the Mouse in the middle.</p>
<p>"Ahem!" said the Mouse with an important air. "Are you all ready? This is the driest
thing I know. Silence all 'round, if you please! 'William the Conqueror, whose cause
was favored by the pope, was soon submitted to by the English, who wanted leaders, and
had been of late much accustomed to usurpation and conquest. Edwin and Morcar, the
Earls of Mercia and Northumbria'&#8212;"</p>
<p>"Ugh!" said the Lory, with a shiver.</p>
<p>"&#8212;'And even Stigand, the patriotic archbishop of Canterbury, found it
advisable'&#8212;"</p>
<p>"Found <i>what</i>?" said the Duck.</p>
<p>"Found <i>it</i>," the Mouse replied rather crossly; "of course, you know what 'it'
means."</p>
<p>"I know what 'it' means well enough, when <i>I</i> find a thing," said the Duck;
"it's generally a frog or a worm. The question is, what did the archbishop find?"</p>
<p>The Mouse did not notice this question, but hurriedly went on, "'&#8212;found it
advisable to go with Edgar Atheling to meet William and offer him the crown.'&#8212;How
are you getting on now, my dear?" it continued, turning to Alice as it spoke.</p>
<p>"As wet as ever," said Alice in a melancholy tone; "it doesn't seem to dry me at
all."</p>
<p>"In that case," said the Dodo solemnly, rising to its feet, "I move that the meeting
adjourn, for the immediate adoption of more energetic remedies&#8212;"<a id="Page_16"
class="pageno" title="[Pg 16]"></a></p>
<p>"Speak English!" said the Eaglet. "I don't know the meaning of half those long
words, and, what's more, I don't believe you do either!"</p>
<p>"What I was going to say," said the Dodo in an offended tone, "is that the best
thing to get us dry would be a Caucus-race."</p>
<p>"What <i>is</i> a Caucus-race?" said Alice.</p>
<figure class="small">
<img src="images/i009_th.jpg" alt="Illo9" />
</figure>
<p>"Why," said the Dodo, "the best way to explain it is to do it." First it marked out
a race-course, in a sort of circle, and then all the party were placed along the
course, here and there. There was no "One, two, three and away!" but they began running
when they liked and left off when they liked, so that it was not easy to know when the
race was over. However, when they had been running half an hour or so and were quite
dry again, the Dodo suddenly called out, "The race is over!" and they all crowded
'round it, panting and asking, "But who has won?"<a id="Page_17" class="pageno" title=
"[Pg 17]"></a></p>
<p>This question the Dodo could not answer without a great deal of thought. At last it
said, "<i>Everybody</i> has won, and <i>all</i> must have prizes."</p>
<p>"But who is to give the prizes?" quite a chorus of voices asked.</p>
<p>"Why, <i>she</i>, of course," said the Dodo, pointing to Alice with one finger; and
the whole party at once crowded 'round her, calling out, in a confused way, "Prizes!
Prizes!"</p>
<p>Alice had no idea what to do, and in despair she put her hand into her pocket and
pulled out a box of comfits (luckily the salt-water had not got into it) and handed
them 'round as prizes. There was exactly one a-piece, all 'round.</p>
<p>The next thing was to eat the comfits; this caused some noise and confusion, as the
large birds complained that they could not taste theirs, and the small ones choked and
had to be patted on the back. However, it was over at last and they sat down again in a
ring and begged the Mouse to tell them something more.</p>
<p>"You promised to tell me your history, you know," said Alice, "and why it is you
hate&#8212;C and D," she added in a whisper, half afraid that it would be offended
again.</p>
<p>"Mine is a long and a sad tale!" said the Mouse, turning to Alice and sighing.</p>
<p>"It <i>is</i> a long tail, certainly," said Alice, looking down with wonder at the
Mouse's tail, "but why do you call it sad?" And she kept on puzzling about it while the
Mouse was speaking, so that her idea of the tale was something like this:<a id=
"Page_18" class="pageno" title="[Pg 18]"></a>&#8212;</p>
<div class="poem stanza">
<span class="i1">"Fury said to<br /></span> <span class="i2">a mouse,
That<br /></span> <span class="i3">he met in the<br /></span> <span class="i4">house,
'Let<br /></span> <span class="i5">us both go<br /></span> <span class="i6">to law:
<i>I</i><br /></span> <span class="i6 c9">will prosecute<br /></span> <span class=
"i6 c9"><i>you</i>.&#8212;<br /></span> <span class="i6 c9">Come, I'll<br /></span>
<span class="i5 c9">take no denial:<br /></span> <span class="c20"><span class=
"i4 c9">We must have<br /></span> <span class="i3 c10">the trial;<br /></span>
<span class="i2 c10">For really<br /></span> <span class="i1 c10">this
morning<br /></span> <span class="i0 c10">I've<br /></span> <span class=
"i0 c10">nothing<br /></span> <span class="i0 c11">to do.'<br /></span> <span class=
"i1 c11">Said the<br /></span> <span class="i2 c11">mouse to<br /></span>
<span class="c19"><span class="i3 c11">the cur,<br /></span> <span class=
"i4 c11">'Such a<br /></span> <span class="i5 c12">trial, dear<br /></span>
<span class="i6 c12">sir, With<br /></span> <span class="i8 c12">no jury<br /></span>
<span class="i9 c12">or judge,<br /></span> <span class="i9 c12">would<br /></span>
<span class="i8 c13">be wasting<br /></span> <span class="i7 c13">our<br /></span>
<span class="c18"><span class="i5 c13">breath.'<br /></span> <span class=
"i4 c13">'I'll be<br /></span> <span class="i3 c13">judge,<br /></span> <span class=
"i2 c14">I'll be<br /></span> <span class="i1 c14">jury,'<br /></span> <span class=
"i0 c14">said<br /></span> <span class="i0 c14">cunning<br /></span> <span class=
"i1 c14">old<br /></span> <span class="i2 c15">Fury;<br /></span> <span class=
"c17"><span class="i3 c15">'I'll<br /></span> <span class="i4 c15">try<br /></span>
<span class="i5 c15">the<br /></span> <span class="i6 c15">whole<br /></span>
<span class="i7 c16">cause,<br /></span> <span class="i7 c16">and<br /></span>
<span class="i6 c16">condemn<br /></span> <span class="i5 c16">you to<br /></span>
<span class="i3 c16">death.'"<br /></span></span></span></span></span>
</div>
<p><a id="Page_19" class="pageno" title="[Pg 19]"></a>"You are not attending!" said the
Mouse to Alice, severely. "What are you thinking of?"</p>
<p>"I beg your pardon," said Alice very humbly, "you had got to the fifth bend, I
think?"</p>
<p>"You insult me by talking such nonsense!" said the Mouse, getting up and walking
away.</p>
<p>"Please come back and finish your story!" Alice called after it. And the others all
joined in chorus, "Yes, please do!" But the Mouse only shook its head impatiently and
walked a little quicker.</p>
<p>"I wish I had Dinah, our cat, here!" said Alice. This caused a remarkable sensation
among the party. Some of the birds hurried off at once, and a Canary called out in a
trembling voice, to its children, "Come away, my dears! It's high time you were all in
bed!" On various pretexts they all moved off and Alice was soon left alone.</p>
<p>"I wish I hadn't mentioned Dinah! Nobody seems to like her down here and I'm sure
she's the best cat in the world!" Poor Alice began to cry again, for she felt very
lonely and low-spirited. In a little while, however, she again heard a little pattering
of footsteps in the distance and she looked up eagerly.</p>
<figure class="small">
<img src="images/i010_th.jpg" alt="Illo10" />
</figure>
<p><a id="Page_20" class="pageno" title="[Pg 20]"></a></p>
<figure class="small">
<img src="images/i011_th.jpg" alt="Illo11" />
</figure>
</section>
</body>
</html>

View file

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00007"><a id="IV_THE_RABBIT_SENDS_IN_A_LITTLE_BILL"></a>
The Rabbit Sends In A Little Bill</h2>
<p>It was the White Rabbit, trotting slowly back again and
looking anxiously about as it went, as if it had lost something; Alice heard it
muttering to itself, "The Duchess! The Duchess! Oh, my dear paws! Oh, my fur and
whiskers! She'll get me executed, as sure as ferrets are ferrets! Where <i>can</i> I
have dropped them, I wonder?" Alice guessed in a moment that it was looking for the fan
and the pair of white kid-gloves and she very good-naturedly began hunting about for
them, but they were nowhere to be seen&#8212;everything seemed to have changed since
her swim in the pool, and the great hall, with the glass table and the little door, had
vanished completely.<a id="Page_21" class="pageno" title="[Pg 21]"></a></p>
<p>Very soon the Rabbit noticed Alice, and called to her, in an angry tone, "Why, Mary
Ann, what <i>are</i> you doing out here? Run home this moment and fetch me a pair of
gloves and a fan! Quick, now!"</p>
<p>"He took me for his housemaid!" said Alice, as she ran off. "How surprised he'll be
when he finds out who I am!" As she said this, she came upon a neat little house, on
the door of which was a bright brass plate with the name "W. RABBIT" engraved upon it.
She went in without knocking and hurried upstairs, in great fear lest she should meet
the real Mary Ann and be turned out of the house before she had found the fan and
gloves.</p>
<p>By this time, Alice had found her way into a tidy little room with a table in the
window, and on it a fan and two or three pairs of tiny white kid-gloves; she took up
the fan and a pair of the gloves and was just going to leave the room, when her eyes
fell upon a little bottle that stood near the looking-glass. She uncorked it and put it
to her lips, saying to herself, "I do hope it'll make me grow large again, for, really,
I'm quite tired of being such a tiny little thing!"</p>
<p>Before she had drunk half the bottle, she found her head pressing against the
ceiling, and had to stoop to save her neck from being broken. She hastily put down the
bottle, remarking, "That's quite enough&#8212;I hope I sha'n't grow any more."</p>
<p>Alas! It was too late to wish that! She went on growing and growing and very soon
she had to kneel down on the floor. Still she went on growing, and, as a last resource,
she put one arm out of the window and one foot up the chimney, and said to
herself,<a id="Page_22" class="pageno" title="[Pg 22]"></a> "Now I can do no more,
wha tev er happens. What <i>will</i> become of me?"</p>
<figure class="small">
<img src="images/i012_th.jpg" alt="Illo12" />
</figure>
<p>Luckily for Alice, the little mag ic b ottle had now had its full effect and she grew
no larger. After a few minutes she hear d a voice outside and stopped to listen.</p>
<p>"Mary Ann! Mary Ann!" said the voice . "Fetch me my gloves this moment!" Thencame a
little pattering of feet on the stairs. Alice knew it was the Rabbit coming to look for
her and she trembled till she shoo k th e house, quite forgetting that she was now about
a thousand times as large as the Rabbit and had no reason to be afraid of it.</p>
<p>Presently the Rabbit came up to the door and tried to open it; but as the door
opened inwards and Alice's elbow was press ed h ard against it, that attempt proved a
failure. Alice heard it sayto itself, "Then I'll g o 'round and get in at the
window."</p>
<p>"<i>That</i> you won't!" thought Ali ce; and after waiting till she fancied she heard
the Rabbit just under the window, sh e suddenly spread out her hand and<a id="Page_23"
class="pageno" title="[Pg 23]"></a > made asnatch in the air. She did not get hold of
anything, but she heard a little s hriek and a fall and a crash of bro ken glass, from
which she concluded that it was just possible it had fallen into a c ucumber-frame or
something of that sort.</p>
<p>Next came an angry voice&#8212;the Rabbit's&#8212;" Pat!Pat! Where are you?" And
then a voice she had never heard before, "Sure t hen, I'm here! D iggi ng for apples, yer
honor!"</p>
<p>"Here! Come and help me out of this! Now tell me, Pat, what's that in the
windo w?"</p>
<p>"Sure, it's an arm, yer honor!"</p>
<p>"Well, it's got no business the re, at any rate; go and take it away!"</p>
<p>There was a long silence after this an d Alice could only hear whispers now and then,
and at last she spread out her hand ag ain and made another snatch in the air. This time
there were <i>two</i> little shrieks a nd more sounds of brok en g lass. "I wonder what
they'll do next!" thought Alice. "As for pulling me out of the wi ndow, I only wish they
<i>could</i>!"</p>
<p>She waited for some time without h earing anything more. At last came a rumbling of
little cart-wheels and the sound of a good many voices all talking together. She made
out the words: "Where's the other lad der? Bill's got the other&#8212;Bill! Here, Bill!
Will the roof bear?&#8212;Who's to g o do wn the chimney?&#8212;Nay, <i>I</i> sha'n't!
<i>You</i> do it! Here, Bill! The master s ays you've got to go down the chimney!"</p>
<p>Alice drew her foot as far down the c himney as she couldand waited till she heard a
little animal scratching and scrambling a bout in the chimney close above
<a id="Page_24" class="pageno" title="[Pg 24]"></a> her; then she gave one sharp kick and waited to see
what would happen next.</p>
<p>The first thing she heard was a general chorus of "There goes Bill!" then the
Rabbit's voice alone&#8212;"Catc h him, you by the hedge!" Then silence a nd t hen another
confusion of voices&#8212;"Hold up his head&#8212;Brandy now&#8212;Don 't choke
him&#8212;What happened to you?"</p>
<p>Lastcame a little f eeble, squeaking voice, "Well, I hardly know&#8212;No mor e,
thank ye. I'm better now&#8212;all I know is, something comes at me like a
Jack-in-the -box and up I goes like a sky-rocket!"</p>
<p>Afte r a minute or two of silence, they began moving about again, and Alice heard the
Rabbit say , "A barrowful will do, to begin with."</p>
<p>"A barrowful of <i>what</i>?" though t Alice. But she had not long to doubt, for the
next moment a shower of little pe bbles came rattling in at the window and some of them
hither in the face. Alice no ticed, with some surprise, that the pebbles were all
turning into little cakes as they lay on the floor and abr ight idea came into her
head. "If Ieat one of these cakes," she thought, "it's sure to m ake <i>some</i> change
in my size."</p>
<p>So she swallowed one of the cakes an d was delighted to find that she began shrinking
directly. As soon as she was small enough to get through the door, she ran out of the
house and found quite a crowd of lit tle animals and birds waiting outside. They all
m ade a rush at Alice the moment she ap peared, but she ran off as hard as she could and
soon found herself safe in a thick wood.</p>
<figure class="full">
<img src="images/plate03_th.jpg" alt="The Duchess tucked her arm affectionately into Alice's." title=
"The Duchess tucked her arm affectionately into Alice's." />
<figcaption>
<p>"The Duchess tucked her arm affectionately into Alice's."</p>
</figcaption>
</figure>
<p>"The first thing I've got to do," said Alice to herself, <a id="Page_25" class=
"pageno" title="[Pg 25]"></a>as she wande red about in the wood, "is to grow to my right
size again; and the second thing i s to find my way into that lovely garden. I suppose I
ought to eat or drink something or other, but the great question is 'What?'"</p>
<p>Alice looked all arou nd her at the flowers and the blades of grass, but she could
notsee anything that loo kedlike the right thing to eat or drink under the
circumstances. There was a large mushroom growing near her, about the same height as
herself. She stretched herself u p on tiptoe and peeped over the edge and her eyes
immediately met those of a large blue caterpillar, that was sitting on the top, with
its armsfolded, quietly smoking a long hookah a nd taking not the smallest notice of
her or of anything else.</p>
<figure class="small">
<img src="images/i013_th.jpg" alt="Illo13" />
</figure>
<p><a id="Page_26" class="pageno" title="[Pg 26]"></a></p>
</section>
</body>
</html>

View file

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00008"><a id="V_ADVICE_FROM_A_CATERPILLAR"></a>
Advice From A Caterpillar</h2>
<p>At last the Caterpillar took the hookah out of its mouth and
addressed Alice in alanguid, sleepy voice.</p>
<p>"Who are <i>you</i>?" said the Caterpillar.</p>
<figure class="small">
<img src="images/i014_th.jpg" alt="Illo14" />
</figure>
<p>Alice replied, rather shyly, "I&#8212;I hardly know, sir, just at present&#8212;at
least I know who I <i>was</i> when I got up this morning, but I think I must have
changed several times since then."</p>
<p> "Wha t do you mean by that?" said the Caterpillar, sternly. "Explain yourself!"<a id=
"Page_27" class="pageno" title="[Pg 27]"></a></p>
<p>"I can't explain<i>myself</i>, I'm afraid, sir," said Alice,"because I'm not
myself, you see&#8212;being so ma nydifferent sizes in a day is very confusing." She
drew hers elf up and said very gravely, "I think you ought to tell me who <i>you</i>
are, first."</p>
<p>"Why?" said the Caterpillar.</p>
<p>As Alice could not think of any good reason and the Caterpillar seemed to be in a
<i>very</i> u npleasant state of mind, she turned away.</p>
<p>"Come back!" the Caterpillar called after her. "I've something important to say!"
Alice turned and came back again.</p>
<p>"Keep your temper," said the Caterpillar.</p>
<p>"Is that all?" said Alice, swallowing down her anger as well as she could.</p>
<p>"No," said the Caterpillar.</p>
<p>It unfolded its arms, took the hookah out of its mouth again, and said, " So you
think you're changed, do you?"</p>
<p>"I'm afr aid, I am, sir," said Alice. "I can't remember things as I used&#8212;and I
don't keep the same size for ten minutes together!"</p>
<p>"What size d o you want to be?" asked the Caterpillar.</p>
<p>"Oh, I'mnot particular as to si ze," Alice hastily replied, "only one doesn't like
changing sooften, you know. I should like to be a <i>little</i> larger, sir, if you
wouldn't mind," said Alice. "Three inches is such a wretched height to be."</p>
<p>"It is a very good height indeed!" said the Caterpillar an grily, rearing itself
upright as it spoke (it was exactly three inches high).<a id="Page_28" class="pageno"
title="[Pg 28]"></a></p>
<p>In a minute or two, the Caterpillar got down off the mushroom and crawled away in to
the grass,merely remarking, as it went, "One side will make you grow taller, and the
other side will make you grow shorter."</p>
<p>"One side of <i>what</i>? The other side of <i >what</i>?" thought Alice to
herself.</p>
<p>"Of the mushroom," said the Caterpillar, just as if she had asked it aloud; and in
another moment, it was out of sight.</p>
<p>Alice remained looking thoughtfully at the mushroom for a minute, trying to make out
whichwere the two sides of it. At last she stretched her arms 'round it as far as they
would go, and broke off a bit of the edge with each hand.</p>
<p>"And now which is which?" she said to herself, and nibbled a little of the
right-hand bit to trythe effect. The next moment she felt a violent blow underneath
her chin&#8212;it had struck her foot!</p>
<p>She was a gooddeal frightened b y this very sudden change, as she was shrinking
rapidly; so she set to work at once to eat some of the other bit. Her chin was pressed
so closely against her foo tthat there was hardly room to open her mouth; but she did
it at last and managed to s wallow a morsel of the left-hand bit....</p>
<p>"Come, my head's free at last!" said Alice; but all she could see, when she loo ked
down, was an immense length of neck, which seemed to rise like a stalk out of a sea of
green leaves that lay far below her.</p>
<p>"Where <i>have</i> my shoulders got to? And oh, mypoor hands, how is it I can't see
you?"Shewas de<a id="Page_29" class="pageno" title="[Pg 29]"></a>lighted to find that
her neck would bend about easily in any direction, like a serpent. She had just
succeeded in curving it down into a gr aceful zigzag and was going to dive inamong the
leaves, when a s harp hiss made her draw back in a hurry &#8212;a large pigeon had flo wn
into her face and was beating her violently with its wings.</p>
<figure class="small">
<img src="images/i015_th.jpg" alt="Illo15" />
</figure>
<p>"Serpent!" cried the Pigeon.</p>
<p>"I'm <i>not</i> a serpent!" said Alice indignantly. "Let me alone!"</p>
<p>"I've tried the roo ts of trees, and I've tried banks, and I've tri ed hedges," the
Pigeon went on, "but those serpents! There's no pleasing them!"</p>
<p>Alice was more and more puzzled.</p>
<p>"As if it wasn't trouble enough hatching the eggs," said the Pigeon, "but I must be
on the look-out for serpents, night and day! And just as I'd taken the highest tree in
the wood," continued the Pigeon, raising its voice to a shriek, "and just as I was
thinkingI should be free of them at last, they must needs come wriggling down from the
sky! Ugh, Serpent!"</p>
<p>"But I'm <i>not</i> a serpent, I tell you!" said Alice. "I'm a&#8212;I'm a&#8212;I'm
a little girl," she added rather<a id="Page_30" class="pageno" title="[Pg 30]"></a>
doubtfully, as she remembered the numbe r of changes she had gone through that day.</p>
<p>"You're looking for eggs, I know <i>that</i> well enough," said the Pigeon; "and
what does it matter to me whether you're a little girl or a serpent?"</p>
<p>"It matters a good deal to <i>me</i>," said Alice h astily; "but I'm not looking for
eggs, as it happens, and if I was, I shouldn't want <i>yours</i>&#8212;I don't like
them raw."</p>
<p>"Well, be off, then!" said the Pige on in a sulky tone, as it settled down again into
its nest. Alice crouched down a mong the trees as well as she could, for her neck kept
getting entangled among the bra nches, and every now and then she had to stop and
untwist it. After awhile she rememb ered that she still held the pieces of mushroom in
her hands, and she set to work very carefully, nibbling first at one and then at the
other, and growing sometimes taller and sometimes shorter, until she had succeeded in
bringing herself down to her usual height.</p>
<p>It was so long since she had been anything near the right size that it felt quite
strange at first. "The next thing is to get into that beautiful garden&#8212;how
<i>is</i> that to be done, I wonder?" As she said this, she came suddenly upon an open
place, with a little house in it about four feet high. "Whoever lives there," thought
Alice, "it'll never do to come upon them <i>this</i> size;why, I should frighten them
out of their wits!" She did not venture to go near the house till she had brought
herself down to nine inches high.</p>
<p><a id="Page_31" class="pageno" title="[Pg 31]"></a></p>
</section>
</body>
</html>

View file

@ -1,122 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00009"><a id="VI_PIG_AND_PEPPER"></a>Pig And Pepper</h2>
<p>For a minute or twoshe stood looking at the house, when
suddenly a footman in livery came running out of the wood (judging by his face only,
she would have called him a fish)&#8212;and rapped loudly at the door with his
knuckles. It was opened by another footman in livery,with a round face and large eyes
like a frog.</p>
<figure class="small">
<img src="images/i015_th.jpg" alt="Illo15" />
</figure>
<p>The Fish-Footman began by producing from under his arm a great letter, and this he
handed over to the other, saying, in a solemn tone, "For the Duchess.<a id="Page_32"
class="pageno" title="[Pg 32]"></a> An invitation from the Queen to play croquet." The
Frog-Footman repeated, in the same solemn tone, "From the Queen. An invitation for the
Duchess to play croquet." Then they both bowed low and their curls got entangled
together.</p>
<p>When Alice next peeped out, the Fish-Footman was gone, and the other was sitting on
the ground near the door, staring stupidly up into the sky. Alice went timidly up to
the door and knocked.</p>
<p>"There's no sortof use in knocking," said the Footman, "and that for two reasons.
First, because I'm on the same side of the door as you are; secondly, because they're
making such a noise inside, no one could possibly hear you." And certainly there
<i>was</i> a most extraordinary noise going on within&#8212;a constant howling and
sneezing, and every now and then a great crash, as if a dish or kettle had been broken
to pieces.</p>
<p>"How am I to get in?" asked Alice.</p>
<p>"<i>Are</i> you to get in at all?" said the Footman. "That's the first question, you
know."</p>
<p>Alice opened the door and went in. The door led right into a large kitchen, which
was full of smoke from one end to the other; the Duchess was sitting on a three-legged
stool in the middle, nursing a baby; the cook was leaning over the fire, stirring a
large caldron which seemed to be full of soup.</p>
<p>"There's certainly too much pepper in that soup!" Alice said to herself, as well as
she could for sneezing. Even the Duchess sneezed occasionally; and asforthe baby, it
was sneezing and howling alternately without a moment'spause. The only two
creatures<a id="Page_33" class="pageno" title="[Pg 33]"></a> in the kitchen that did
<i>not</i> sneeze were the cook and a large cat, which was grinning from ear to
ear.</p>
<p>"Please would you tell me," said Alice, a little timidly, "why your cat grins like
that?"</p>
<p>"It's a Cheshire-Cat," said the Duchess,"and that's why."</p>
<p>"I didn't know that Cheshire-Cats always grinned; in fact, I didn't know that cats
<i>could</i> grin," said Alice.</p>
<p>"You don't know much," said the Duchess, "and that's a fact."</p>
<p>Just then the cook took the caldron of soupoff the fire, and at once set to work
throwing everything within her reach at the Duchess and the baby&#8212;the fire-irons
came first; then followed a shower of saucepans, plates and dishes. The Duchess took no
notice of them, even when they hit her, and the baby was howling so much already that
itwas quite impossible to say whether the blows hurt it or not.</p>
<p>"Oh, <i>please</i> mind what you're doing!" cried Alice, jumping up and down in an
agony of terror.</p>
<p>"Here! You may nurse it a bit, if you like!" the Duchess said to Alice, flinging the
baby at her as she spoke. "I must go and get ready to play croquet with the Queen," and
she hurried out of the room.</p>
<p>Alice caught the baby with some difficulty, as it was a queer-shaped little creature
and held out its arms and legs in all directions. "If I don't take this child away with
me," thought Alice, "they're sure to kill it in a day or two. Wouldn't it be murder to
leave it behind?" She said the last words out loud and the little thing grunted in
reply.<a id="Page_34" class="pageno" title="[Pg 34]"></a></p>
<p>"If you're going to turn into a pig, my dear," said Alice, "I'll have nothing more
to do with you. Mind now!"</p>
<p>Alice was just beginning to think to herself, "Now, what am I to do with this
creature, when I get it home?" when it grunted again so violently that Alice looked
down into its face in some alarm. This time there could be <i>no</i> mistake about
it&#8212;it was neither more nor less than a pig; so she set the little creature down
and felt quite relieved to see it trot awayquietly into the wood.</p>
<p>Alice was a little startled by seeing the Cheshire-Catsitting on a bough of a tree
a few yards off. The Cat only grinned when it saw her. "Cheshire-Puss," began Alice,
rather timidly, "would you please tell me which way I ought to gofrom here?"</p>
<p>"In <i>that</i> direction," the Cat said, waving the right paw 'round, "lives a
Hatter; and in <i>that</i> direction," waving the other paw, "lives a March Hare. Visit
either you like; they're both mad."</p>
<p>"But I don't want to go among mad people," Alice remarked.</p>
<p>"Oh, you can't help that," said the Cat; "we're all mad here. Do you play croquet
with the Queen to-day?"</p>
<p>"I should like it very much," said Alice, "but I haven't been invited yet."</p>
<p>"You'll see me there," said the Cat, and vanished.</p>
<p>Alice had not gone much farther before she came in sight of the house of the March
Hare; it was so large a house that she did not like to go near till she had nibbled
some more of the left-hand bit of mushroom.</p>
<p><a id="Page_35" class="pageno" title="[Pg 35]"></a></p>
</section>
</body>
</html>

View file

@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00010"><a id="VII_A_MAD_TEA-PARTY"></a>A Mad Tea-Party</h2>
<p>There was a table set out under a tree in front of the
house, and the March Hare and the Hatter were having tea at it; a Dormouse was sitting
between them, fast asleep.</p>
<p>The table was a large one, but the three were all crowded together at one corner of
it. "No room! No room!" they cried out when they saw Alice coming. "There's
<i>plenty</i> of room!" said Alice indignantly, and she sat down in a large arm-chair
at one end of the table.</p>
<p>The Hatter opened his eyes very wide on hearing this, but all he said was "Why is a
raven like a writing-desk?"</p>
<p>"I'm glad they've begun asking riddles&#8212;I believe I can guess that," she added
aloud.</p>
<p>"Do you mean that you think you can find out the answer to it?" said the March
Hare.</p>
<p>"Exactly so," said Alice.</p>
<p>"Then you should say what you mean," the March Hare went on.</p>
<p>"I do," Alice hastily replied; "at least&#8212;at least I mean what I
say&#8212;that's the same thing, you know."</p>
<p>"You might just as well say," added the Dormouse, which seemed to be talking in its
sleep, "that 'I breathe when I sleep' is the same thing as 'I sleep when I
breathe!'"<a id="Page_36" class="pageno" title="[Pg36]"></a></p>
<p>"It <i>is</i> the same thing with you," said the Hatter, and he poured a little hot
tea upon its nose. The Dormouse shook its head impatiently and said, without opening
its eyes,"Ofcourse, of course; just what I was going to remark myself."</p>
<figure class="small">
<img src="images/i017_th.jpg" alt="Illo17" />
</figure>
<p>"Have you guessed the riddle yet?" the Hatter said, turning to Alice again.</p>
<p>"No, I give it up," Alice replied. "What's theanswer?"</p>
<p>"I haven't the slightest idea," said the Hatter.</p>
<p>"Nor I,"said the March Hare.</p>
<p>Alice gave a weary sigh. "I think you might do something better with the time," she
said, "than wasting it in asking riddles that have no answers."</p>
<p>"Take some more tea," the March Hare said to Alice, very earnestly.</p>
<p>"I've had nothing yet," Alice replied in an offended tone, "so I can't take
more."</p>
<p>"You mean you can't take <i>less</i>," said the Hatter; "it's very easy to take
<i>more</i> than nothing."</p>
<p>At this, Alice got up and walked off. The Dormouse fell asleep instantly and neither
of the others took the least notice of her going, thoughshe looked <a id="Page_37"
class="pageno" title="[Pg 37]"></a>back once or twice; the last time she saw them, they
were trying to put the Dormouse into the tea-pot.</p>
<figure class="full">
<img src="images/plate04_th.jpg" alt="The Trial of the Knave of Hearts." title="The Trial of the Knave of Hearts." />
<figcaption>
<p>The Trial of the Knave of Hearts.</p>
</figcaption>
</figure>
<p>"At any rate, I'll never go <i>there</i> again!" said Alice, as she picked her way
through the wood. "It's the stupidest tea-party I ever was at in all my life!" Just as
she said this, she noticed that one of the trees had a door leading right into it.
"That's very curious!" she thought. "I think I may as well go in at once." And in she
went.</p>
<p>Once more she foundherself in the long hall and close to the little glass table.
Taking the little golden key, she unlocked the door that led into the garden. Then she
set to work nibbling at the mushroom (she had kept a piece of it in her pocket) till
she was about a foot high; then she walked down the little passage; and
<i>then</i>&#8212;she found herself at last in the beautiful garden, among the bright
flower-beds and the cool fountains.</p>
</section>
</body>
</html>

View file

@ -1,151 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00011"><a id="VIII_THE_QUEENS_CROQUET_GROUND"></a>
The Queen's Croquet Ground</h2>
<p>A large rose-tree stood near the entrance of the garden; the
roses growing on it were white, but there were threegardeners at it, busily painting
them red. Suddenly their eyes chanced to fall upon Alice, as she stood watching them.
"Would you tell me, please," said Alice, a little timidly, "why you are painting those
roses?"</p>
<p>Five and Seven said nothing, butlooked at Two.<a id="Page_38" class="pageno" title=
"[Pg 38]"></a> Twobegan, in a low voice, "Why, the fact is, you see, Miss, this here
ought to have been a <i>red</i> rose-tree, and we put a white one in by mistake; and,
if the Queen was to find it out, we should all have our heads cut off, you know. So you
see, Miss, we're doing our best, afore she comes, to&#8212;" At this moment, Five, who
had been anxiously looking across the garden, called out, "The Queen! The Queen!" and
the three gardeners instantly threw themselves flat upon their faces. There was a sound
of many footstepsand Alice looked 'round, eager to see the Queen.</p>
<p>First came ten soldiers carrying clubs, with their hands and feet at the corners:
next the ten courtiers; these were ornamented all over with diamonds. After these came
the royal children; there were ten of them, all ornamented with hearts. Next came the
guests, mostly Kings and Queens, and among them Alice recognized the White Rabbit. Then
followed the Knave of Hearts, carrying the King's crown on a crimson velvet cushion;
and last of all this grand procession came THE KING AND THE QUEEN OF HEARTS.</p>
<p>When the procession came opposite to Alice, they all stopped and looked at her, and
the Queen said severely, "Who is this?" She said it to the Knave of Hearts, who only
bowed and smiled in reply.</p>
<p>"My name is Alice, so please Your Majesty," said Alice very politely; but she added
to herself, "Why, they're only a pack of cards, after all!"</p>
<p>"Can you play croquet?" shouted the Queen. The question was evidently meant for
Alice.<a id="Page_39" class="pageno" title="[Pg 39]"></a></p>
<p>"Yes!" said Alice loudly.</p>
<p>"Come on, then!" roared the Queen.</p>
<p>"It's&#8212;it's a very fine day!" said a timid voice to Alice. She was walking by
the White Rabbit, who was peeping anxiously into her face.</p>
<p>"Very," said Alice. "Where's the Duchess?"</p>
<p>"Hush! Hush!" said the Rabbit. "She's under sentence of execution."</p>
<p>"What for?" said Alice.</p>
<p>"She boxed the Queen's ears&#8212;" the Rabbit began.</p>
<p>"Get to your places!" shouted the Queen in a voice of thunder, and people began
running about in all directions, tumbling up against each other. However, they got
settled down in a minute or two, and the game began.</p>
<p>Alice thought she had never seen such a curious croquet-ground in her life; it was
all ridges and furrows. The croquet balls were live hedgehogs, and the mallets live
flamingos and the soldiers had to double themselves up and stand on their hands and
feet, to make the arches.</p>
<p>The players all played at once, without waiting for turns, quarrelling all the while
and fighting for the hedgehogs; and in a very short time, the Queen was in a furious
passion and went stamping about and shouting, "Off with his head!" or "Off with her
head!" about once in a minute.</p>
<p>"They're dreadfully fond of beheading people here," thought Alice; "the great wonder
is that there's anyone left alive!"</p>
<p>She was looking about for some way of escape, when she noticed a curious appearance
in the air.<a id="Page_40" class="pageno" title="[Pg 40]"></a> "It's the Cheshire-Cat,"
she said to herself; "now I shall have somebody to talk to."</p>
<p>"How are you getting on?" said the Cat.</p>
<p>"I don't think they play at all fairly," Alice said, in a rather complaining tone;
"and they all quarrel so dreadfully one can't hear oneself speak&#8212;and they don't
seem to have any rules in particular."</p>
<p>"How do you like the Queen?" said the Cat in a low voice.</p>
<p>"Not at all," said Alice.</p>
<figure class="small">
<img src="images/i018_th.jpg" alt="Illo18" />
</figure>
<p>Alice thought she might as well go back and see how the game was going on. So she
went off in search of her hedgehog. The hedgehog was engaged in a fight with another
hedgehog, which seemed to Alice an excellent opportunity for croqueting one of them
with the other; the only difficulty was that her flamingo was gone across to the other
side of the garden, where Alice could see it trying, in a helpless sort of way, to fly
up into a tree. She caught the flamingo and tucked it away under her arm, that it might
not escape again.<a id="Page_41" class="pageno" title="[Pg 41]"></a></p>
<p>Just then Alice ran across the Duchess (who was now out of prison). She tucked her
arm affectionately into Alice's and they walked off together. Alice was very glad to
find her in such a pleasant temper. She was a little startled, however, when she heard
the voice of the Duchess close to her ear. "You're thinking about something, my dear,
and that makes you forget to talk."</p>
<p>"The game's going on rather better now," Alice said, by way of keeping up the
conversation a little.</p>
<p>"'Tis so," said the Duchess; "and the moral of that is&#8212;'Oh, 'tis love, 'tis
love that makes the world go 'round!'"</p>
<p>"Somebody said," Alice whispered, "that it's done by everybody minding his own
business!"</p>
<p>"Ah, well! It means much the same thing," said the Duchess, digging her sharp little
chin into Alice's shoulder, as she added "and the moral of <i>that</i> is&#8212;'Take
care of the sense and the sounds will take care of themselves.'"</p>
<p>To Alice's great surprise, the Duchess's arm that was linked into hers began to
tremble. Alice looked up and there stood the Queen in front of them, with her arms
folded, frowning like a thunderstorm!</p>
<p>"Now, I give you fair warning," shouted the Queen, stamping on the ground as she
spoke, "either you or your head must be off, and that in about half no time. Take your
choice!" The Duchess took her choice, and was gone in a moment.</p>
<p>"Let's go on with the game," the Queen said to Alice; and Alice was too much
frightened to say a<a id="Page_42" class="pageno" title="[Pg 42]"></a> word, but slowly
followed her back to the croquet-ground.</p>
<p>All the time they were playing, the Queen never left off quarreling with the other
players and shouting, "Off with his head!" or "Off with her head!" By the end of half
an hour or so, all the players, except the King, the Queen and Alice, were in custody
of the soldiers and under sentence of execution.</p>
<p>Then the Queen left off, quite out of breath, and walked away with Alice.</p>
<p>Alice heard the King say in a low voice to the company generally, "You are all
pardoned."</p>
<p>Suddenly the cry "The Trial's beginning!" was heard in the distance, and Alice ran
along with the others.</p>
</section>
</body>
</html>

View file

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00012"><a id="IX_WHO_STOLE_THE_TARTS"></a>Who Stole The Tarts?</h2>
<p>The King and Queen of Hearts were seated on their throne
when they arrived, with a great crowd assembled about them&#8212;all sorts of little
birds and beasts, as well as the whole pack of cards: the Knave was standing before
them, in chains, with a soldier on each side to guard him; and near the King was the
White Rabbit, with a trumpet in one hand and a scroll of parchment in the other. In the
very middle of the court<a id="Page_43" class="pageno" title="[Pg 43]"></a> was a
table, with a large dish of tarts upon it. "I wish they'd get the trial done," Alice
thought, "and hand 'round the refreshments!"</p>
<figure class="small">
<img src="images/i019_th.jpg" alt="Illo19" />
</figure>
<p>The judge, by the way, was the King and he wore his crown over his great wig.
"That's the jury-box," thought Alice; "and those twelve creatures (some were animals
and some were birds) I suppose they are the jurors."</p>
<p>Just then the White Rabbit cried out "Silence in the court!"</p>
<p>"Herald, read the accusation!" said the King.</p>
<p>On this, the White Rabbit blew three blasts on the trumpet, then unrolled the
parchment-scroll and read as follows:</p>
<div class="poem stanza">
<span class="i0">"The Queen of Hearts, she made some tarts,<br /></span> <span class=
"i2">All on a summer day;<br /></span> <span class="i0">The Knave of Hearts, he stole
those tarts<br /></span> <span class="i2">And took them quite away!"<br /></span>
</div>
<p>"Call the first witness," said the King; and the White Rabbit blew three blasts on
the trumpet and called out, "First witness!"</p>
<p>The first witness was the Hatter. He came in with<a id="Page_44" class="pageno"
title="[Pg 44]"></a> a teacup in one hand and a piece of bread and butter in the
other.</p>
<p>"You ought to have finished," said the King. "When did you begin?"</p>
<p>The Hatter looked at the March Hare, who had followed him into the court, arm in arm
with the Dormouse. "Fourteenth of March, I <i>think</i> it was," he said.</p>
<p>"Give your evidence," said the King, "and don't be nervous, or I'll have you
executed on the spot."</p>
<p>This did not seem to encourage the witness at all; he kept shifting from one foot to
the other, looking uneasily at the Queen, and, in his confusion, he bit a large piece
out of his teacup instead of the bread and butter.</p>
<p>Just at this moment Alice felt a very curious sensation&#8212;she was beginning to
grow larger again.</p>
<p>The miserable Hatter dropped his teacup and bread and butter and went down on one
knee. "I'm a poor man, Your Majesty," he began.</p>
<p>"You're a <i>very</i> poor <i>speaker</i>," said the King.</p>
<p>"You may go," said the King, and the Hatter hurriedly left the court.</p>
<p>"Call the next witness!" said the King.</p>
<p>The next witness was the Duchess's cook. She carried the pepper-box in her hand and
the people near the door began sneezing all at once.</p>
<p>"Give your evidence," said the King.</p>
<p>"Sha'n't," said the cook.</p>
<p>The King looked anxiously at the White Rabbit, who said, in a low voice, "Your
Majesty must cross-examine <i>this</i> witness."<a id="Page_45" class="pageno" title=
"[Pg 45]"></a></p>
<p>"Well, if I must, I must," the King said. "What are tarts made of?"</p>
<p>"Pepper, mostly," said the cook.</p>
<p>For some minutes the whole court was in confusion and by the time they had settled
down again, the cook had disappeared.</p>
<p>"Never mind!" said the King, "call the next witness."</p>
<p>Alice watched the White Rabbit as he fumbled over the list. Imagine her surprise
when he read out, at the top of his shrill little voice, the name "Alice!"</p>
</section>
</body>
</html>

View file

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Alice's Adventures in Wonderland</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css"/>
<meta charset="utf-8"/>
</head>
<body>
<section epub:type="chapter">
<h2 id="pgepubid00013"><a id="X_ALICES_EVIDENCE"></a>Alice's Evidence</h2>
<p>"Here!" cried Alice. She jumped up in such a hurry that she
tipped over the jury-box, upsetting all the jurymen on to the heads of the crowd
below.</p>
<p>"Oh, I <i>beg</i> your pardon!" she exclaimed in a tone of great dismay.</p>
<p>"The trial cannot proceed," said the King, "until all the jurymen are back in their
proper places&#8212;<i>all</i>," he repeated with great emphasis, looking hard at
Alice.</p>
<p>"What do you know about this business?" the King said to Alice.</p>
<p>"Nothing whatever," said Alice.</p>
<p>The King then read from his book: "Rule forty-<a id="Page_46" class="pageno" title=
"[Pg 46]"></a>two. <i>All persons more than a mile high to leave the court</i>."</p>
<p>"<i>I'm</i> not a mile high," said Alice.</p>
<p>"Nearly two miles high," said the Queen.</p>
<figure>
<img src="images/i020_th.jpg" alt="Illo20" />
</figure>
<p>"Well, I sha'n't go, at any rate," said Alice.</p>
<p>The King turned pale and shut his note-book hastily. "Consider your verdict," he
said to the jury, in a low, trembling voice.</p>
<p>"There's more evidence to come yet, please Your Majesty," said the White Rabbit,
jumping up in a great hurry. "This paper has just been picked up. It seems to be a
letter written by the prisoner to&#8212;to somebody." He unfolded the paper as he spoke
and added, "It isn't a letter, after all; it's a set of verses."</p>
<p>"Please, Your Majesty," said the Knave, "I didn't write it and they can't prove that
I did; there's no name signed at the end."</p>
<p>"You <i>must</i> have meant some mischief, or else you'd have signed your name like
an honest man," said the King. There was a general clapping of hands at this.<a id=
"Page_47" class="pageno" title="[Pg 47]"></a></p>
<p>"Read them," he added, turning to the White Rabbit.</p>
<p>There was dead silence in the court whilst the White Rabbit read out the verses.</p>
<p>"That's the most important piece of evidence we've heard yet," said the King.</p>
<p>"<i>I</i> don't believe there's an atom of meaning in it," ventured Alice.</p>
<p>"If there's no meaning in it," said the King, "that saves a world of trouble, you
know, as we needn't try to find any. Let the jury consider their verdict."</p>
<p>"No, no!" said the Queen. "Sentence first&#8212;verdict afterwards."</p>
<p>"Stuff and nonsense!" said Alice loudly. "The idea of having the sentence
first!"</p>
<figure class="small">
<img src="images/ii021_th.jpg" alt="Illo21" />
</figure>
<p>"Hold your tongue!" said the Queen, turning purple.</p>
<p>"I won't!" said Alice.</p>
<p>"Off with her head!" the Queen shouted at the top of her voice. Nobody moved.</p>
<p>"Who cares for <i>you</i>?" said Alice (she had grown to her full size by this
time). "You're nothing but a pack of cards!"</p>
<p>At this, the whole pack rose up in the air and came flying down upon her; she<a id=
"Page_48" class="pageno" title="[Pg 48]"></a> gave a little scream, half of fright and
half of anger, and tried to beat them off, and found herself lying on the bank, with
her head in the lap of her sister, who was gently brushing away some dead leaves that
had fluttered down from the trees upon her face.</p>
<p>"Wake up, Alice dear!" said her sister. "Why, what a long sleep you've had!"</p>
<p>"Oh, I've had such a curious dream!" said Alice. And she told her sister, as well as
she could remember them, all these strange adventures of hers that you have just been
reading about. Alice got up and ran off, thinking while she ran, as well she might,
what a wonderful dream it had been.</p>
<figure class="small">
<img src="images/i022_th.jpg" alt="Illo22" />
</figure>
</section>
</body>
</html>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<title>Cover</title>
<link rel="stylesheet" type="text/css" href="css/stylesheet.css"/>
<meta charset="utf-8"/>
</head>
<body>
<img src="images/cover_th.jpg" alt="Cover Image" title="Cover Image"/>
</body>
</html>

View file

@ -1,105 +0,0 @@
body {
font-family: Georgia, serif;
font-size: 1em;
line-height: 1.33em;
}
/* Book Title */
h1 {
font-size: 1.5em;
line-height: 1.33em;
}
/* Chapter Title */
h2 {
font-size: 1.33em;
line-height: 1.2em;
}
/* Subtitle */
h3 {
font-size: 1.25em;
line-height: 1.12em;
}
/* Meta Info */
h4 {
font-size: 1.1em;
line-height: 1.05em;
}
/* Chapter Container */
section {
}
section > p {
}
/* Drop Cap */
section > p:first-of-type:first-letter {
float: left;
font-size: 4em;
line-height: .8;
padding: 0 .2em;
font-family: Georgia;
}
figure.small {
}
figure.full {
}
figure > img {
}
figcaption {
}
figcaption > p {
}
/* More specific Kindle Eink queries at: http://epubsecrets.com/media-queries-for-kindle-devices.php */
/* Amazon Kindle */
@media amzn-kf8 {
}
/* Many device available at: https://css-tricks.com/snippets/css/media-queries-for-standard-devices/ */
/* Smartphone - Portrait */
@media only screen
and (min-device-width: 320px)
and (max-device-width: 667px)
and (orientation: portrait) {
}
/* Smartphone - Landscape */
@media only screen
and (min-device-width: 320px)
and (max-device-width: 667px)
and (orientation: landscape) {
}
/* Tablet - Portrait and Landscape */
@media only screen
and (min-device-width: 768px)
and (max-device-width: 1024px) {
}
/* Laptop & Desktops */
@media only screen
and (min-device-width: 1025px) {
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

Some files were not shown because too many files have changed in this diff Show more