got it to build, quick n dirty; epubjs patch

This commit is contained in:
wommy 2024-09-24 02:57:02 -04:00
parent c67c586c98
commit 2baf50da12
43 changed files with 14616 additions and 203 deletions

View file

@ -15,7 +15,7 @@
</template> </template>
<script> <script>
import ePub from 'epubjs' import ePub from '../../static/libs/epubjs-patch/index.js'
/** /**
* @typedef {object} EpubReader * @typedef {object} EpubReader

View file

@ -124,7 +124,8 @@ export default defineNuxtConfig({
autoprefixer: {} autoprefixer: {}
} }
} }
} },
transpile: ['luxon', 'cookie-es']
}, },
watchers: { watchers: {
webpack: { webpack: {
@ -137,7 +138,10 @@ export default defineNuxtConfig({
host: '0.0.0.0' host: '0.0.0.0'
}, },
bridge: false, bridge: {
transpile: true,
nitro: true
},
/** /**
* Temporary workaround for @nuxt-community/tailwindcss-module. * Temporary workaround for @nuxt-community/tailwindcss-module.

232
client/package-lock.json generated
View file

@ -30,6 +30,8 @@
"@nuxt/bridge": "^3.3.1", "@nuxt/bridge": "^3.3.1",
"@nuxtjs/pwa": "^3.3.5", "@nuxtjs/pwa": "^3.3.5",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"nuxi": "^3.13.2", "nuxi": "^3.13.2",
"postcss": "^8.3.6", "postcss": "^8.3.6",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
@ -4123,13 +4125,6 @@
"node": "^14.16.0 || ^16.10.0 || >=17.0.0" "node": "^14.16.0 || ^16.10.0 || >=17.0.0"
} }
}, },
"node_modules/@nuxt/bridge-schema/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/bridge-schema/node_modules/scule": { "node_modules/@nuxt/bridge-schema/node_modules/scule": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
@ -4191,13 +4186,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/@nuxt/bridge/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/bridge/node_modules/enhanced-resolve": { "node_modules/@nuxt/bridge/node_modules/enhanced-resolve": {
"version": "5.17.1", "version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@ -4268,13 +4256,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@nuxt/bridge/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/bridge/node_modules/node-fetch": { "node_modules/@nuxt/bridge/node_modules/node-fetch": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
@ -4560,12 +4541,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/cli/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/@nuxt/cli/node_modules/fs-extra": { "node_modules/@nuxt/cli/node_modules/fs-extra": {
"version": "11.2.0", "version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
@ -4580,6 +4555,12 @@
"node": ">=14.14" "node": ">=14.14"
} }
}, },
"node_modules/@nuxt/cli/node_modules/hookable": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-4.4.1.tgz",
"integrity": "sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==",
"license": "MIT"
},
"node_modules/@nuxt/components": { "node_modules/@nuxt/components": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@nuxt/components/-/components-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@nuxt/components/-/components-2.2.1.tgz",
@ -4666,12 +4647,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/config/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/@nuxt/core": { "node_modules/@nuxt/core": {
"version": "2.18.1", "version": "2.18.1",
"resolved": "https://registry.npmjs.org/@nuxt/core/-/core-2.18.1.tgz", "resolved": "https://registry.npmjs.org/@nuxt/core/-/core-2.18.1.tgz",
@ -4714,6 +4689,12 @@
"node": ">=14.14" "node": ">=14.14"
} }
}, },
"node_modules/@nuxt/core/node_modules/hookable": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-4.4.1.tgz",
"integrity": "sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==",
"license": "MIT"
},
"node_modules/@nuxt/devalue": { "node_modules/@nuxt/devalue": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
@ -4839,12 +4820,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/generator/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/@nuxt/generator/node_modules/fs-extra": { "node_modules/@nuxt/generator/node_modules/fs-extra": {
"version": "11.2.0", "version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
@ -4901,13 +4876,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/kit/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/kit/node_modules/globby": { "node_modules/@nuxt/kit/node_modules/globby": {
"version": "14.0.2", "version": "14.0.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz",
@ -5021,6 +4989,13 @@
"semver": "^7.3.4" "semver": "^7.3.4"
} }
}, },
"node_modules/@nuxt/postcss8/node_modules/defu": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/defu/-/defu-3.2.2.tgz",
"integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/postcss8/node_modules/postcss-import": { "node_modules/@nuxt/postcss8/node_modules/postcss-import": {
"version": "13.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-13.0.0.tgz", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-13.0.0.tgz",
@ -5073,20 +5048,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/schema/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/schema/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxt/schema/node_modules/scule": { "node_modules/@nuxt/schema/node_modules/scule": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
@ -5180,11 +5141,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/telemetry/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/@nuxt/telemetry/node_modules/dotenv": { "node_modules/@nuxt/telemetry/node_modules/dotenv": {
"version": "9.0.2", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
@ -5369,12 +5325,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/@nuxt/vue-renderer/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/@nuxt/vue-renderer/node_modules/fs-extra": { "node_modules/@nuxt/vue-renderer/node_modules/fs-extra": {
"version": "11.2.0", "version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
@ -5499,6 +5449,13 @@
"workbox-cdn": "^5.1.4" "workbox-cdn": "^5.1.4"
} }
}, },
"node_modules/@nuxtjs/pwa/node_modules/defu": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/defu/-/defu-3.2.2.tgz",
"integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxtjs/youch": { "node_modules/@nuxtjs/youch": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/@nuxtjs/youch/-/youch-4.2.3.tgz", "resolved": "https://registry.npmjs.org/@nuxtjs/youch/-/youch-4.2.3.tgz",
@ -5757,7 +5714,7 @@
}, },
"node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": {
"version": "1.1.0", "version": "1.1.0",
"extraneous": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT" "license": "MIT"
}, },
@ -6769,13 +6726,6 @@
"url": "https://github.com/sponsors/harlan-zw" "url": "https://github.com/sponsors/harlan-zw"
} }
}, },
"node_modules/@unhead/schema/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@unhead/shared": { "node_modules/@unhead/shared": {
"version": "1.11.5", "version": "1.11.5",
"resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.5.tgz", "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.5.tgz",
@ -6823,20 +6773,6 @@
"vue": ">=2.7 || >=3" "vue": ">=2.7 || >=3"
} }
}, },
"node_modules/@unhead/vue/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/@unhead/vue/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vercel/nft": { "node_modules/@vercel/nft": {
"version": "0.26.5", "version": "0.26.5",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz",
@ -8757,13 +8693,6 @@
} }
} }
}, },
"node_modules/c12/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/cac": { "node_modules/cac": {
"version": "6.7.14", "version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@ -10828,10 +10757,10 @@
} }
}, },
"node_modules/defu": { "node_modules/defu": {
"version": "3.2.2", "version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-3.2.2.tgz", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true "license": "MIT"
}, },
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
@ -12697,13 +12626,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/giget/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/git-config-path": { "node_modules/git-config-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz",
@ -12884,13 +12806,6 @@
"unenv": "^1.9.0" "unenv": "^1.9.0"
} }
}, },
"node_modules/h3/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/hard-source-webpack-plugin": { "node_modules/hard-source-webpack-plugin": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz",
@ -13327,9 +13242,10 @@
} }
}, },
"node_modules/hookable": { "node_modules/hookable": {
"version": "4.4.1", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-4.4.1.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/html-entities": { "node_modules/html-entities": {
@ -14738,13 +14654,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/listhen/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/listhen/node_modules/get-port-please": { "node_modules/listhen/node_modules/get-port-please": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz",
@ -15915,13 +15824,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/mkdist/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/mkdist/node_modules/esbuild": { "node_modules/mkdist/node_modules/esbuild": {
"version": "0.23.1", "version": "0.23.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
@ -16619,13 +16521,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/nitropack/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/nitropack/node_modules/esbuild": { "node_modules/nitropack/node_modules/esbuild": {
"version": "0.20.2", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
@ -16730,13 +16625,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/nitropack/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nitropack/node_modules/mime": { "node_modules/nitropack/node_modules/mime": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz",
@ -20427,11 +20315,6 @@
"destr": "^2.0.3" "destr": "^2.0.3"
} }
}, },
"node_modules/rc9/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -21242,12 +21125,6 @@
"defu": "^6.1.4" "defu": "^6.1.4"
} }
}, },
"node_modules/serve-placeholder/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@ -23449,13 +23326,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/unbuild/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/unbuild/node_modules/esbuild": { "node_modules/unbuild/node_modules/esbuild": {
"version": "0.19.12", "version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
@ -23515,13 +23385,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/unbuild/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unbuild/node_modules/pretty-bytes": { "node_modules/unbuild/node_modules/pretty-bytes": {
"version": "6.1.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
@ -23681,13 +23544,6 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/unenv/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/unenv/node_modules/mime": { "node_modules/unenv/node_modules/mime": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@ -23726,13 +23582,6 @@
"url": "https://github.com/sponsors/harlan-zw" "url": "https://github.com/sponsors/harlan-zw"
} }
}, },
"node_modules/unhead/node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -24108,13 +23957,6 @@
"untyped": "dist/cli.mjs" "untyped": "dist/cli.mjs"
} }
}, },
"node_modules/untyped/node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"license": "MIT"
},
"node_modules/untyped/node_modules/scule": { "node_modules/untyped/node_modules/scule": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",

View file

@ -5,11 +5,11 @@
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "nuxt2", "dev": "nuxi dev",
"dev2": "nuxt2 --hostname localhost --port 1337", "dev2": "nuxi dev --hostname localhost --port 1337",
"build": "nuxt2 build", "build": "nuxi build",
"start": "nuxt2 start", "start": "nuxi preview",
"generate": "nuxt2 generate", "generate": "nuxi generate",
"test": "npm run compile-tailwind && cypress run --component --browser chrome", "test": "npm run compile-tailwind && cypress run --component --browser chrome",
"test-visually": "npm run compile-tailwind && cypress open --component --browser chrome", "test-visually": "npm run compile-tailwind && cypress open --component --browser chrome",
"compile-tailwind": "tailwindcss -i ./assets/tailwind.css -o ./cypress/support/tailwind.compiled.css" "compile-tailwind": "tailwindcss -i ./assets/tailwind.css -o ./cypress/support/tailwind.compiled.css"
@ -38,6 +38,8 @@
"@nuxt/bridge": "^3.3.1", "@nuxt/bridge": "^3.3.1",
"@nuxtjs/pwa": "^3.3.5", "@nuxtjs/pwa": "^3.3.5",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"nuxi": "^3.13.2", "nuxi": "^3.13.2",
"postcss": "^8.3.6", "postcss": "^8.3.6",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"

View file

@ -0,0 +1,302 @@
import { EventEmitter } from "events";
import EpubCFI from "./epubcfi";
import { EVENTS } from "./utils/constants";
/**
* Handles managing adding & removing Annotations
* @param {Rendition} rendition
* @class
*/
class Annotations {
constructor (rendition) {
this.rendition = rendition;
this.highlights = [];
this.underlines = [];
this.marks = [];
this._annotations = {};
this._annotationsBySectionIndex = {};
this.rendition.hooks.render.register(this.inject.bind(this));
this.rendition.hooks.unloaded.register(this.clear.bind(this));
}
/**
* Add an annotation to store
* @param {string} type Type of annotation to add: "highlight", "underline", "mark"
* @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
* @param {object} data Data to assign to annotation
* @param {function} [cb] Callback after annotation is added
* @param {string} className CSS class to assign to annotation
* @param {object} styles CSS styles to assign to annotation
* @returns {Annotation} annotation
*/
add (type, cfiRange, data, cb, className, styles) {
let hash = encodeURI(cfiRange + type);
let cfi = new EpubCFI(cfiRange);
let sectionIndex = cfi.spinePos;
let annotation = new Annotation({
type,
cfiRange,
data,
sectionIndex,
cb,
className,
styles
});
this._annotations[hash] = annotation;
if (sectionIndex in this._annotationsBySectionIndex) {
this._annotationsBySectionIndex[sectionIndex].push(hash);
} else {
this._annotationsBySectionIndex[sectionIndex] = [hash];
}
let views = this.rendition.views();
views.forEach( (view) => {
if (annotation.sectionIndex === view.index) {
annotation.attach(view);
}
});
return annotation;
}
/**
* Remove an annotation from store
* @param {EpubCFI} cfiRange EpubCFI range the annotation is attached to
* @param {string} type Type of annotation to add: "highlight", "underline", "mark"
*/
remove (cfiRange, type) {
let hash = encodeURI(cfiRange + type);
if (hash in this._annotations) {
let annotation = this._annotations[hash];
if (type && annotation.type !== type) {
return;
}
let views = this.rendition.views();
views.forEach( (view) => {
this._removeFromAnnotationBySectionIndex(annotation.sectionIndex, hash);
if (annotation.sectionIndex === view.index) {
annotation.detach(view);
}
});
delete this._annotations[hash];
}
}
/**
* Remove an annotations by Section Index
* @private
*/
_removeFromAnnotationBySectionIndex (sectionIndex, hash) {
this._annotationsBySectionIndex[sectionIndex] = this._annotationsAt(sectionIndex).filter(h => h !== hash);
}
/**
* Get annotations by Section Index
* @private
*/
_annotationsAt (index) {
return this._annotationsBySectionIndex[index];
}
/**
* Add a highlight to the store
* @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
* @param {object} data Data to assign to annotation
* @param {function} cb Callback after annotation is clicked
* @param {string} className CSS class to assign to annotation
* @param {object} styles CSS styles to assign to annotation
*/
highlight (cfiRange, data, cb, className, styles) {
return this.add("highlight", cfiRange, data, cb, className, styles);
}
/**
* Add a underline to the store
* @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
* @param {object} data Data to assign to annotation
* @param {function} cb Callback after annotation is clicked
* @param {string} className CSS class to assign to annotation
* @param {object} styles CSS styles to assign to annotation
*/
underline (cfiRange, data, cb, className, styles) {
return this.add("underline", cfiRange, data, cb, className, styles);
}
/**
* Add a mark to the store
* @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
* @param {object} data Data to assign to annotation
* @param {function} cb Callback after annotation is clicked
*/
mark (cfiRange, data, cb) {
return this.add("mark", cfiRange, data, cb);
}
/**
* iterate over annotations in the store
*/
each () {
return this._annotations.forEach.apply(this._annotations, arguments);
}
/**
* Hook for injecting annotation into a view
* @param {View} view
* @private
*/
inject (view) {
let sectionIndex = view.index;
if (sectionIndex in this._annotationsBySectionIndex) {
let annotations = this._annotationsBySectionIndex[sectionIndex];
annotations.forEach((hash) => {
let annotation = this._annotations[hash];
annotation.attach(view);
});
}
}
/**
* Hook for removing annotation from a view
* @param {View} view
* @private
*/
clear (view) {
let sectionIndex = view.index;
if (sectionIndex in this._annotationsBySectionIndex) {
let annotations = this._annotationsBySectionIndex[sectionIndex];
annotations.forEach((hash) => {
let annotation = this._annotations[hash];
annotation.detach(view);
});
}
}
/**
* [Not Implemented] Show annotations
* @TODO: needs implementation in View
*/
show () {
}
/**
* [Not Implemented] Hide annotations
* @TODO: needs implementation in View
*/
hide () {
}
}
/**
* Annotation object
* @class
* @param {object} options
* @param {string} options.type Type of annotation to add: "highlight", "underline", "mark"
* @param {EpubCFI} options.cfiRange EpubCFI range to attach annotation to
* @param {object} options.data Data to assign to annotation
* @param {int} options.sectionIndex Index in the Spine of the Section annotation belongs to
* @param {function} [options.cb] Callback after annotation is clicked
* @param {string} className CSS class to assign to annotation
* @param {object} styles CSS styles to assign to annotation
* @returns {Annotation} annotation
*/
class Annotation extends EventEmitter {
constructor ({
type,
cfiRange,
data,
sectionIndex,
cb,
className,
styles
}) {
super();
this.type = type;
this.cfiRange = cfiRange;
this.data = data;
this.sectionIndex = sectionIndex;
this.mark = undefined;
this.cb = cb;
this.className = className;
this.styles = styles;
}
/**
* Update stored data
* @param {object} data
*/
update (data) {
this.data = data;
}
/**
* Add to a view
* @param {View} view
*/
attach (view) {
let {cfiRange, data, type, mark, cb, className, styles} = this;
let result;
if (type === "highlight") {
result = view.highlight(cfiRange, data, cb, className, styles);
} else if (type === "underline") {
result = view.underline(cfiRange, data, cb, className, styles);
} else if (type === "mark") {
result = view.mark(cfiRange, data, cb);
}
this.mark = result;
this.emit(EVENTS.ANNOTATION.ATTACH, result);
return result;
}
/**
* Remove from a view
* @param {View} view
*/
detach (view) {
let {cfiRange, type} = this;
let result;
if (view) {
if (type === "highlight") {
result = view.unhighlight(cfiRange);
} else if (type === "underline") {
result = view.ununderline(cfiRange);
} else if (type === "mark") {
result = view.unmark(cfiRange);
}
}
this.mark = undefined;
this.emit(EVENTS.ANNOTATION.DETACH, result);
return result;
}
/**
* [Not Implemented] Get text of an annotation
* @TODO: needs implementation in contents
*/
text () {
}
}
Object.assign(Annotation.prototype, EventEmitter.prototype);
export default Annotations

View file

@ -0,0 +1,255 @@
import {defer, isXml, parse} from "./utils/core";
import request from "./utils/request";
import mime from "./utils/mime";
import Path from "./utils/path";
import JSZip from "jszip/dist/jszip";
/**
* Handles Unzipping a requesting files from an Epub Archive
* @class
*/
class Archive {
constructor() {
this.zip = undefined;
this.urlCache = {};
this.checkRequirements();
}
/**
* Checks to see if JSZip exists in global namspace,
* Requires JSZip if it isn't there
* @private
*/
checkRequirements(){
try {
this.zip = new JSZip();
} catch (e) {
throw new Error("JSZip lib not loaded");
}
}
/**
* Open an archive
* @param {binary} input
* @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded
* @return {Promise} zipfile
*/
open(input, isBase64){
return this.zip.loadAsync(input, {"base64": isBase64});
}
/**
* Load and Open an archive
* @param {string} zipUrl
* @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded
* @return {Promise} zipfile
*/
openUrl(zipUrl, isBase64){
return request(zipUrl, "binary")
.then(function(data){
return this.zip.loadAsync(data, {"base64": isBase64});
}.bind(this));
}
/**
* Request a url from the archive
* @param {string} url a url to request from the archive
* @param {string} [type] specify the type of the returned result
* @return {Promise<Blob | string | JSON | Document | XMLDocument>}
*/
request(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);
}
if (response) {
response.then(function (r) {
let result = this.handleResponse(r, type);
deferred.resolve(result);
}.bind(this));
} else {
deferred.reject({
message : "File not found in the epub: " + 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 Archive by Url
* @param {string} url
* @param {string} [mimeType]
* @return {Blob}
*/
getBlob(url, mimeType){
var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
var entry = this.zip.file(decodededUrl);
if(entry) {
mimeType = mimeType || mime.lookup(entry.name);
return entry.async("uint8array").then(function(uint8array) {
return new Blob([uint8array], {type : mimeType});
});
}
}
/**
* Get Text from Archive by Url
* @param {string} url
* @param {string} [encoding]
* @return {string}
*/
getText(url, encoding){
var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
var entry = this.zip.file(decodededUrl);
if(entry) {
return entry.async("string").then(function(text) {
return text;
});
}
}
/**
* Get a base64 encoded result from Archive by Url
* @param {string} url
* @param {string} [mimeType]
* @return {string} base64 encoded
*/
getBase64(url, mimeType){
var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
var entry = this.zip.file(decodededUrl);
if(entry) {
mimeType = mimeType || mime.lookup(entry.name);
return entry.async("base64").then(function(data) {
return "data:" + mimeType + ";base64," + data;
});
}
}
/**
* Create a Url from an unarchived 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 the epub: " + url,
stack : new Error().stack
});
}
return deferred.promise;
}
/**
* Revoke Temp Url for a archive item
* @param {string} url url of the item in the archive
*/
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.zip = undefined;
this.urlCache = {};
}
}
export default Archive;

View file

@ -0,0 +1,769 @@
import { EventEmitter } from "events";
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 extends EventEmitter {
constructor(url, options) {
super();
// 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
Object.assign(Book.prototype, EventEmitter.prototype);
export default Book;

View file

@ -0,0 +1,50 @@
import path from "path-webpack";
import {qs} from "./utils/core";
/**
* Handles Parsing and Accessing an Epub Container
* @class
* @param {document} [containerDocument] xml document
*/
class Container {
constructor(containerDocument) {
this.packagePath = '';
this.directory = '';
this.encoding = '';
if (containerDocument) {
this.parse(containerDocument);
}
}
/**
* Parse the Container XML
* @param {document} containerDocument
*/
parse(containerDocument){
//-- <rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/>
var rootfile;
if(!containerDocument) {
throw new Error("Container File Not Found");
}
rootfile = qs(containerDocument, "rootfile");
if(!rootfile) {
throw new Error("No RootFile Found");
}
this.packagePath = rootfile.getAttribute("full-path");
this.directory = path.dirname(this.packagePath);
this.encoding = containerDocument.xmlEncoding;
}
destroy() {
this.packagePath = undefined;
this.directory = undefined;
this.encoding = undefined;
}
}
export default Container;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
import {qs, qsa } from "./utils/core";
/**
* Open DisplayOptions Format Parser
* @class
* @param {document} displayOptionsDocument XML
*/
class DisplayOptions {
constructor(displayOptionsDocument) {
this.interactive = "";
this.fixedLayout = "";
this.openToSpread = "";
this.orientationLock = "";
if (displayOptionsDocument) {
this.parse(displayOptionsDocument);
}
}
/**
* Parse XML
* @param {document} displayOptionsDocument XML
* @return {DisplayOptions} self
*/
parse(displayOptionsDocument) {
if(!displayOptionsDocument) {
return this;
}
const displayOptionsNode = qs(displayOptionsDocument, "display_options");
if(!displayOptionsNode) {
return this;
}
const options = qsa(displayOptionsNode, "option");
options.forEach((el) => {
let value = "";
if (el.childNodes.length) {
value = el.childNodes[0].nodeValue;
}
switch (el.attributes.name.value) {
case "interactive":
this.interactive = value;
break;
case "fixed-layout":
this.fixedLayout = value;
break;
case "open-to-spread":
this.openToSpread = value;
break;
case "orientation-lock":
this.orientationLock = value;
break;
}
});
return this;
}
destroy() {
this.interactive = undefined;
this.fixedLayout = undefined;
this.openToSpread = undefined;
this.orientationLock = undefined;
}
}
export default DisplayOptions;

View file

@ -0,0 +1,35 @@
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;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +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;
export {
Book,
EpubCFI,
Rendition,
Contents,
Layout
};

View file

@ -0,0 +1,261 @@
import { extend } from "./utils/core";
import { EVENTS } from "./utils/constants";
import { EventEmitter } from "events";
/**
* Figures out the CSS values to apply for a layout
* @class
* @param {object} settings
* @param {string} [settings.layout='reflowable']
* @param {string} [settings.spread]
* @param {number} [settings.minSpreadWidth=800]
* @param {boolean} [settings.evenSpreads=false]
*/
class Layout extends EventEmitter {
constructor(settings) {
super();
this.settings = settings;
this.name = settings.layout || "reflowable";
this._spread = (settings.spread === "none") ? false : true;
this._minSpreadWidth = settings.minSpreadWidth || 800;
this._evenSpreads = settings.evenSpreads || false;
if (settings.flow === "scrolled" ||
settings.flow === "scrolled-continuous" ||
settings.flow === "scrolled-doc") {
this._flow = "scrolled";
} else {
this._flow = "paginated";
}
this.width = 0;
this.height = 0;
this.spreadWidth = 0;
this.delta = 0;
this.columnWidth = 0;
this.gap = 0;
this.divisor = 1;
this.props = {
name: this.name,
spread: this._spread,
flow: this._flow,
width: 0,
height: 0,
spreadWidth: 0,
delta: 0,
columnWidth: 0,
gap: 0,
divisor: 1
};
}
/**
* Switch the flow between paginated and scrolled
* @param {string} flow paginated | scrolled
* @return {string} simplified flow
*/
flow(flow) {
if (typeof(flow) != "undefined") {
if (flow === "scrolled" ||
flow === "scrolled-continuous" ||
flow === "scrolled-doc") {
this._flow = "scrolled";
} else {
this._flow = "paginated";
}
// this.props.flow = this._flow;
this.update({flow: this._flow});
}
return this._flow;
}
/**
* Switch between using spreads or not, and set the
* width at which they switch to single.
* @param {string} spread "none" | "always" | "auto"
* @param {number} min integer in pixels
* @return {boolean} spread true | false
*/
spread(spread, min) {
if (spread) {
this._spread = (spread === "none") ? false : true;
// this.props.spread = this._spread;
this.update({spread: this._spread});
}
if (min >= 0) {
this._minSpreadWidth = min;
}
return this._spread;
}
/**
* Calculate the dimensions of the pagination
* @param {number} _width width of the rendering
* @param {number} _height height of the rendering
* @param {number} _gap width of the gap between columns
*/
calculate(_width, _height, _gap){
var divisor = 1;
var gap = _gap || 0;
//-- Check the width and create even width columns
// var fullWidth = Math.floor(_width);
var width = _width;
var height = _height;
var section = Math.floor(width / 12);
var columnWidth;
var spreadWidth;
var pageWidth;
var delta;
if (this._spread && width >= this._minSpreadWidth) {
divisor = 2;
} else {
divisor = 1;
}
if (this.name === "reflowable" && this._flow === "paginated" && !(_gap >= 0)) {
gap = ((section % 2 === 0) ? section : section - 1);
}
if (this.name === "pre-paginated" ) {
gap = 0;
}
//-- Double Page
if(divisor > 1) {
// width = width - gap;
// columnWidth = (width - gap) / divisor;
// gap = gap / divisor;
columnWidth = (width / divisor) - gap;
pageWidth = columnWidth + gap;
} else {
columnWidth = width;
pageWidth = width;
}
if (this.name === "pre-paginated" && divisor > 1) {
width = columnWidth;
}
spreadWidth = (columnWidth * divisor) + gap;
delta = width;
this.width = width;
this.height = height;
this.spreadWidth = spreadWidth;
this.pageWidth = pageWidth;
this.delta = delta;
this.columnWidth = columnWidth;
this.gap = gap;
this.divisor = divisor;
// this.props.width = width;
// this.props.height = _height;
// this.props.spreadWidth = spreadWidth;
// this.props.pageWidth = pageWidth;
// this.props.delta = delta;
//
// this.props.columnWidth = colWidth;
// this.props.gap = gap;
// this.props.divisor = divisor;
this.update({
width,
height,
spreadWidth,
pageWidth,
delta,
columnWidth,
gap,
divisor
});
}
/**
* Apply Css to a Document
* @param {Contents} contents
* @return {Promise}
*/
format(contents, section, axis){
var formating;
if (this.name === "pre-paginated") {
formating = contents.fit(this.columnWidth, this.height, section);
} else if (this._flow === "paginated") {
formating = contents.columns(this.width, this.height, this.columnWidth, this.gap, this.settings.direction);
} else if (axis && axis === "horizontal") {
formating = contents.size(null, this.height);
} else {
formating = contents.size(this.width, null);
}
return formating; // might be a promise in some View Managers
}
/**
* Count number of pages
* @param {number} totalLength
* @param {number} pageLength
* @return {{spreads: Number, pages: Number}}
*/
count(totalLength, pageLength) {
let spreads, pages;
if (this.name === "pre-paginated") {
spreads = 1;
pages = 1;
} else if (this._flow === "paginated") {
pageLength = pageLength || this.delta;
spreads = Math.ceil( totalLength / pageLength);
pages = spreads * this.divisor;
} else { // scrolled
pageLength = pageLength || this.height;
spreads = Math.ceil( totalLength / pageLength);
pages = spreads;
}
return {
spreads,
pages
};
}
/**
* Update props that have changed
* @private
* @param {object} props
*/
update(props) {
// Remove props that haven't changed
Object.keys(props).forEach((propName) => {
if (this.props[propName] === props[propName]) {
delete props[propName];
}
});
if(Object.keys(props).length > 0) {
let newProps = extend(this.props, props);
this.emit(EVENTS.LAYOUT.UPDATED, newProps, props);
}
}
}
Object.assign(Layout.prototype, EventEmitter.prototype);
export default Layout;

View file

@ -0,0 +1,27 @@
Copyright (c) 2013, FuturePress
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.

View file

@ -0,0 +1,502 @@
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 "events";
/**
* Find Locations for a Book
* @param {Spine} spine
* @param {request} request
* @param {number} [pause=100]
*/
class Locations extends EventEmitter {
constructor(spine, request, pause) {
super();
this.spine = spine;
this.request = request;
this.pause = pause || 100;
this.q = new Queue(this);
this.epubcfi = new EpubCFI();
this._locations = [];
this._locationsWords = [];
this.total = 0;
this.break = 150;
this._current = 0;
this._wordCounter = 0;
this.currentLocation = '';
this._currentCfi ='';
this.processingTimeout = undefined;
}
/**
* Load all of sections in the book to generate locations
* @param {int} chars how many chars to split on
* @return {Promise<Array<string>>} locations
*/
generate(chars) {
if (chars) {
this.break = chars;
}
this.q.pause();
this.spine.each(function(section) {
if (section.linear) {
this.q.enqueue(this.process.bind(this), section);
}
}.bind(this));
return this.q.run().then(function() {
this.total = this._locations.length - 1;
if (this._currentCfi) {
this.currentLocation = this._currentCfi;
}
return this._locations;
// console.log(this.percentage(this.book.rendition.location.start), this.percentage(this.book.rendition.location.end));
}.bind(this));
}
createRange () {
return {
startContainer: undefined,
startOffset: undefined,
endContainer: undefined,
endOffset: undefined
};
}
process(section) {
return section.load(this.request)
.then(function(contents) {
var completed = new defer();
var locations = this.parse(contents, section.cfiBase);
this._locations = this._locations.concat(locations);
section.unload();
this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause);
return completed.promise;
}.bind(this));
}
parse(contents, cfiBase, chars) {
var locations = [];
var range;
var doc = contents.ownerDocument;
var body = qs(doc, "body");
var counter = 0;
var prev;
var _break = chars || this.break;
var parser = function(node) {
var len = node.length;
var dist;
var pos = 0;
if (node.textContent.trim().length === 0) {
return false; // continue
}
// Start range
if (counter == 0) {
range = this.createRange();
range.startContainer = node;
range.startOffset = 0;
}
dist = _break - counter;
// Node is smaller than a break,
// skip over it
if(dist > len){
counter += len;
pos = len;
}
while (pos < len) {
dist = _break - counter;
if (counter === 0) {
// Start new range
pos += 1;
range = this.createRange();
range.startContainer = node;
range.startOffset = pos;
}
// pos += dist;
// Gone over
if(pos + dist >= len){
// Continue counter for next node
counter += len - pos;
// break
pos = len;
// At End
} else {
// Advance pos
pos += dist;
// End the previous range
range.endContainer = node;
range.endOffset = pos;
// cfi = section.cfiFromRange(range);
let cfi = new EpubCFI(range, cfiBase).toString();
locations.push(cfi);
counter = 0;
}
}
prev = node;
};
sprint(body, parser.bind(this));
// Close remaining
if (range && range.startContainer && prev) {
range.endContainer = prev;
range.endOffset = prev.length;
let cfi = new EpubCFI(range, cfiBase).toString();
locations.push(cfi);
counter = 0;
}
return locations;
}
/**
* Load all of sections in the book to generate locations
* @param {string} startCfi start position
* @param {int} wordCount how many words to split on
* @param {int} count result count
* @return {object} locations
*/
generateFromWords(startCfi, wordCount, count) {
var start = startCfi ? new EpubCFI(startCfi) : undefined;
this.q.pause();
this._locationsWords = [];
this._wordCounter = 0;
this.spine.each(function(section) {
if (section.linear) {
if (start) {
if (section.index >= start.spinePos) {
this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count);
}
} else {
this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count);
}
}
}.bind(this));
return this.q.run().then(function() {
if (this._currentCfi) {
this.currentLocation = this._currentCfi;
}
return this._locationsWords;
}.bind(this));
}
processWords(section, wordCount, startCfi, count) {
if (count && this._locationsWords.length >= count) {
return Promise.resolve();
}
return section.load(this.request)
.then(function(contents) {
var completed = new defer();
var locations = this.parseWords(contents, section, wordCount, startCfi);
var remainingCount = count - this._locationsWords.length;
this._locationsWords = this._locationsWords.concat(locations.length >= count ? locations.slice(0, remainingCount) : locations);
section.unload();
this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause);
return completed.promise;
}.bind(this));
}
//http://stackoverflow.com/questions/18679576/counting-words-in-string
countWords(s) {
s = s.replace(/(^\s*)|(\s*$)/gi, "");//exclude start and end white-space
s = s.replace(/[ ]{2,}/gi, " ");//2 or more space to 1
s = s.replace(/\n /, "\n"); // exclude newline with a start spacing
return s.split(" ").length;
}
parseWords(contents, section, wordCount, startCfi) {
var cfiBase = section.cfiBase;
var locations = [];
var doc = contents.ownerDocument;
var body = qs(doc, "body");
var prev;
var _break = wordCount;
var foundStartNode = startCfi ? startCfi.spinePos !== section.index : true;
var startNode;
if (startCfi && section.index === startCfi.spinePos) {
startNode = startCfi.findNode(startCfi.range ? startCfi.path.steps.concat(startCfi.start.steps) : startCfi.path.steps, contents.ownerDocument);
}
var parser = function(node) {
if (!foundStartNode) {
if (node === startNode) {
foundStartNode = true;
} else {
return false;
}
}
if (node.textContent.length < 10) {
if (node.textContent.trim().length === 0) {
return false;
}
}
var len = this.countWords(node.textContent);
var dist;
var pos = 0;
if (len === 0) {
return false; // continue
}
dist = _break - this._wordCounter;
// Node is smaller than a break,
// skip over it
if (dist > len) {
this._wordCounter += len;
pos = len;
}
while (pos < len) {
dist = _break - this._wordCounter;
// Gone over
if (pos + dist >= len) {
// Continue counter for next node
this._wordCounter += len - pos;
// break
pos = len;
// At End
} else {
// Advance pos
pos += dist;
let cfi = new EpubCFI(node, cfiBase);
locations.push({ cfi: cfi.toString(), wordCount: this._wordCounter });
this._wordCounter = 0;
}
}
prev = node;
};
sprint(body, parser.bind(this));
return locations;
}
/**
* 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, this.epubcfi.compare);
if (loc > this.total) {
return this.total;
}
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.total) {
return 0;
}
return (loc / this.total);
}
/**
* 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.total]);
cfi.collapse();
return cfi.toString();
}
loc = Math.ceil(this.total * percentage);
return this.cfiFromLocation(loc);
}
/**
* Load locations from JSON
* @param {json} locations
*/
load(locations){
if (typeof locations === "string") {
this._locations = JSON.parse(locations);
} else {
this._locations = locations;
}
this.total = this._locations.length - 1;
return this._locations;
}
/**
* Save locations to JSON
* @return {json}
*/
save(){
return JSON.stringify(this._locations);
}
getCurrent(){
return this._current;
}
setCurrent(curr){
var loc;
if(typeof curr == "string"){
this._currentCfi = curr;
} else if (typeof curr == "number") {
this._current = curr;
} else {
return;
}
if(this._locations.length === 0) {
return;
}
if(typeof curr == "string"){
loc = this.locationFromCfi(curr);
this._current = loc;
} else {
loc = curr;
}
this.emit(EVENTS.LOCATIONS.CHANGED, {
percentage: this.percentageFromLocation(loc)
});
}
/**
* Get the current location
*/
get currentLocation() {
return this._current;
}
/**
* Set the current location
*/
set currentLocation(curr) {
this.setCurrent(curr);
}
/**
* Locations length
*/
length () {
return this._locations.length;
}
destroy () {
this.spine = undefined;
this.request = undefined;
this.pause = undefined;
this.q.stop();
this.q = undefined;
this.epubcfi = undefined;
this._locations = undefined
this.total = undefined;
this.break = undefined;
this._current = undefined;
this.currentLocation = undefined;
this._currentCfi = undefined;
clearTimeout(this.processingTimeout);
}
}
Object.assign(Locations.prototype, EventEmitter.prototype);
export default Locations;

View file

@ -0,0 +1,592 @@
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";
class ContinuousViewManager extends DefaultViewManager {
constructor(options) {
super(options);
this.name = "continuous";
this.settings = extend(this.settings || {}, {
infinite: true,
overflow: undefined,
axis: undefined,
writingMode: undefined,
flow: "scrolled",
offset: 500,
offsetDelta: 250,
width: undefined,
height: undefined,
snap: false,
afterScrolledTimeout: 10,
allowScriptedContent: false,
allowPopups: false
});
extend(this.settings, options.settings || {});
// Gap can be 0, but defaults doesn't handle that
if (options.settings.gap != "undefined" && options.settings.gap === 0) {
this.settings.gap = options.settings.gap;
}
this.viewSettings = {
ignoreClass: this.settings.ignoreClass,
axis: this.settings.axis,
flow: this.settings.flow,
layout: this.layout,
width: 0,
height: 0,
forceEvenPages: false,
allowScriptedContent: this.settings.allowScriptedContent,
allowPopups: this.settings.allowPopups
};
this.scrollTop = 0;
this.scrollLeft = 0;
}
display(section, target){
return DefaultViewManager.prototype.display.call(this, section, target)
.then(function () {
return this.fill();
}.bind(this));
}
fill(_full){
var full = _full || new defer();
this.q.enqueue(() => {
return this.check();
}).then((result) => {
if (result) {
this.fill(full);
} else {
full.resolve();
}
});
return full.promise;
}
moveTo(offset){
// var bounds = this.stage.bounds();
// var dist = Math.floor(offset.top / bounds.height) * bounds.height;
var distX = 0,
distY = 0;
var offsetX = 0,
offsetY = 0;
if(!this.isPaginated) {
distY = offset.top;
offsetY = offset.top+this.settings.offsetDelta;
} else {
distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
offsetX = distX+this.settings.offsetDelta;
}
if (distX > 0 || distY > 0) {
this.scrollBy(distX, distY, true);
}
}
afterResized(view){
this.emit(EVENTS.MANAGERS.RESIZE, view.section);
}
// Remove Previous Listeners if present
removeShownListeners(view){
// view.off("shown", this.afterDisplayed);
// view.off("shown", this.afterDisplayedAbove);
view.onDisplayed = function(){};
}
add(section){
var view = this.createView(section);
this.views.append(view);
view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
view.expanded = true;
});
view.on(EVENTS.VIEWS.AXIS, (axis) => {
this.updateAxis(axis);
});
view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
this.updateWritingMode(mode);
});
// view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
view.onDisplayed = this.afterDisplayed.bind(this);
view.onResize = this.afterResized.bind(this);
return view.display(this.request);
}
append(section){
var view = this.createView(section);
view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
view.expanded = true;
});
view.on(EVENTS.VIEWS.AXIS, (axis) => {
this.updateAxis(axis);
});
view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
this.updateWritingMode(mode);
});
this.views.append(view);
view.onDisplayed = this.afterDisplayed.bind(this);
return view;
}
prepend(section){
var view = this.createView(section);
view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
this.counter(bounds);
view.expanded = true;
});
view.on(EVENTS.VIEWS.AXIS, (axis) => {
this.updateAxis(axis);
});
view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
this.updateWritingMode(mode);
});
this.views.prepend(view);
view.onDisplayed = this.afterDisplayed.bind(this);
return view;
}
counter(bounds){
if(this.settings.axis === "vertical") {
this.scrollBy(0, bounds.heightDelta, true);
} else {
this.scrollBy(bounds.widthDelta, 0, true);
}
}
update(_offset){
var container = this.bounds();
var views = this.views.all();
var viewsLength = views.length;
var visible = [];
var offset = typeof _offset != "undefined" ? _offset : (this.settings.offset || 0);
var isVisible;
var view;
var updating = new defer();
var promises = [];
for (var i = 0; i < viewsLength; i++) {
view = views[i];
isVisible = this.isVisible(view, offset, offset, container);
if(isVisible === true) {
// console.log("visible " + view.index, view.displayed);
if (!view.displayed) {
let displayed = view.display(this.request)
.then(function (view) {
view.show();
}, (err) => {
view.hide();
});
promises.push(displayed);
} else {
view.show();
}
visible.push(view);
} else {
this.q.enqueue(view.destroy.bind(view));
// console.log("hidden " + view.index, view.displayed);
clearTimeout(this.trimTimeout);
this.trimTimeout = setTimeout(function(){
this.q.enqueue(this.trim.bind(this));
}.bind(this), 250);
}
}
if(promises.length){
return Promise.all(promises)
.catch((err) => {
updating.reject(err);
});
} else {
updating.resolve();
return updating.promise;
}
}
check(_offsetLeft, _offsetTop){
var checking = new defer();
var newViews = [];
var horizontal = (this.settings.axis === "horizontal");
var delta = this.settings.offset || 0;
if (_offsetLeft && horizontal) {
delta = _offsetLeft;
}
if (_offsetTop && !horizontal) {
delta = _offsetTop;
}
var bounds = this._bounds; // bounds saved this until resize
let offset = horizontal ? this.scrollLeft : this.scrollTop;
let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height;
let contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight;
let writingMode = (this.writingMode && this.writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
let rtlScrollType = this.settings.rtlScrollType;
let rtl = this.settings.direction === "rtl";
if (!this.settings.fullsize) {
// Scroll offset starts at width of element
if (rtl && rtlScrollType === "default" && writingMode === "horizontal") {
offset = contentLength - visibleLength - offset;
}
// Scroll offset starts at 0 and goes negative
if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") {
offset = offset * -1;
}
} else {
// Scroll offset starts at 0 and goes negative
if ((horizontal && rtl && rtlScrollType === "negative") ||
(!horizontal && rtl && rtlScrollType === "default")) {
offset = offset * -1;
}
}
let prepend = () => {
let first = this.views.first();
let prev = first && first.section.prev();
if(prev) {
newViews.push(this.prepend(prev));
}
};
let append = () => {
let last = this.views.last();
let next = last && last.section.next();
if(next) {
newViews.push(this.append(next));
}
};
let end = offset + visibleLength + delta;
let start = offset - delta;
if (end >= contentLength) {
append();
}
if (start < 0) {
prepend();
}
let promises = newViews.map((view) => {
return view.display(this.request);
});
if(newViews.length){
return Promise.all(promises)
.then(() => {
return this.check();
})
.then(() => {
// Check to see if anything new is on screen after rendering
return this.update(delta);
}, (err) => {
return err;
});
} else {
this.q.enqueue(function(){
this.update();
}.bind(this));
checking.resolve(false);
return checking.promise;
}
}
trim(){
var task = new defer();
var displayed = this.views.displayed();
var first = displayed[0];
var last = displayed[displayed.length-1];
var firstIndex = this.views.indexOf(first);
var lastIndex = this.views.indexOf(last);
var above = this.views.slice(0, firstIndex);
var below = this.views.slice(lastIndex+1);
// Erase all but last above
for (var i = 0; i < above.length-1; i++) {
this.erase(above[i], above);
}
// Erase all except first below
for (var j = 1; j < below.length; j++) {
this.erase(below[j]);
}
task.resolve();
return task.promise;
}
erase(view, above){ //Trim
var prevTop;
var prevLeft;
if(!this.settings.fullsize) {
prevTop = this.container.scrollTop;
prevLeft = this.container.scrollLeft;
} else {
prevTop = window.scrollY;
prevLeft = window.scrollX;
}
var bounds = view.bounds();
this.views.remove(view);
if(above) {
if (this.settings.axis === "vertical") {
this.scrollTo(0, prevTop - bounds.height, true);
} else {
if(this.settings.direction === 'rtl') {
if (!this.settings.fullsize) {
this.scrollTo(prevLeft, 0, true);
} else {
this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true);
}
} else {
this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true);
}
}
}
}
addEventListeners(stage){
window.addEventListener("unload", function(e){
this.ignore = true;
// this.scrollTo(0,0);
this.destroy();
}.bind(this));
this.addScrollListeners();
if (this.isPaginated && this.settings.snap) {
this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
}
}
addScrollListeners() {
var scroller;
this.tick = requestAnimationFrame;
let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
this.scrollDeltaVert = 0;
this.scrollDeltaHorz = 0;
if(!this.settings.fullsize) {
scroller = this.container;
this.scrollTop = this.container.scrollTop;
this.scrollLeft = this.container.scrollLeft;
} else {
scroller = window;
this.scrollTop = window.scrollY * dir;
this.scrollLeft = window.scrollX * dir;
}
this._onScroll = this.onScroll.bind(this);
scroller.addEventListener("scroll", this._onScroll);
this._scrolled = debounce(this.scrolled.bind(this), 30);
// this.tick.call(window, this.onScroll.bind(this));
this.didScroll = false;
}
removeEventListeners(){
var scroller;
if(!this.settings.fullsize) {
scroller = this.container;
} else {
scroller = window;
}
scroller.removeEventListener("scroll", this._onScroll);
this._onScroll = undefined;
}
onScroll(){
let scrollTop;
let scrollLeft;
let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
if(!this.settings.fullsize) {
scrollTop = this.container.scrollTop;
scrollLeft = this.container.scrollLeft;
} else {
scrollTop = window.scrollY * dir;
scrollLeft = window.scrollX * dir;
}
this.scrollTop = scrollTop;
this.scrollLeft = scrollLeft;
if(!this.ignore) {
this._scrolled();
} else {
this.ignore = false;
}
this.scrollDeltaVert += Math.abs(scrollTop-this.prevScrollTop);
this.scrollDeltaHorz += Math.abs(scrollLeft-this.prevScrollLeft);
this.prevScrollTop = scrollTop;
this.prevScrollLeft = scrollLeft;
clearTimeout(this.scrollTimeout);
this.scrollTimeout = setTimeout(function(){
this.scrollDeltaVert = 0;
this.scrollDeltaHorz = 0;
}.bind(this), 150);
clearTimeout(this.afterScrolled);
this.didScroll = false;
}
scrolled() {
this.q.enqueue(function() {
return this.check();
}.bind(this));
this.emit(EVENTS.MANAGERS.SCROLL, {
top: this.scrollTop,
left: this.scrollLeft
});
clearTimeout(this.afterScrolled);
this.afterScrolled = setTimeout(function () {
// Don't report scroll if we are about the snap
if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) {
return;
}
this.emit(EVENTS.MANAGERS.SCROLLED, {
top: this.scrollTop,
left: this.scrollLeft
});
}.bind(this), this.settings.afterScrolledTimeout);
}
next(){
let delta = this.layout.props.name === "pre-paginated" &&
this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
if(!this.views.length) return;
if(this.isPaginated && this.settings.axis === "horizontal") {
this.scrollBy(delta, 0, true);
} else {
this.scrollBy(0, this.layout.height, true);
}
this.q.enqueue(function() {
return this.check();
}.bind(this));
}
prev(){
let delta = this.layout.props.name === "pre-paginated" &&
this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
if(!this.views.length) return;
if(this.isPaginated && this.settings.axis === "horizontal") {
this.scrollBy(-delta, 0, true);
} else {
this.scrollBy(0, -this.layout.height, true);
}
this.q.enqueue(function() {
return this.check();
}.bind(this));
}
updateFlow(flow){
if (this.rendered && this.snapper) {
this.snapper.destroy();
this.snapper = undefined;
}
super.updateFlow(flow, "scroll");
if (this.rendered && this.isPaginated && this.settings.snap) {
this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
}
}
destroy(){
super.destroy();
if (this.snapper) {
this.snapper.destroy();
}
}
}
export default ContinuousViewManager;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,339 @@
import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core";
import { EVENTS, DOM_EVENTS } from "../../utils/constants";
import { EventEmitter } from "events";
// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
const PI_D2 = (Math.PI / 2);
const EASING_EQUATIONS = {
easeOutSine: function (pos) {
return Math.sin(pos * PI_D2);
},
easeInOutSine: function (pos) {
return (-0.5 * (Math.cos(Math.PI * pos) - 1));
},
easeInOutQuint: function (pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 5);
}
return 0.5 * (Math.pow((pos - 2), 5) + 2);
},
easeInCubic: function(pos) {
return Math.pow(pos, 3);
}
};
class Snap extends EventEmitter {
constructor(manager, options) {
super();
this.settings = extend({
duration: 80,
minVelocity: 0.2,
minDistance: 10,
easing: EASING_EQUATIONS['easeInCubic']
}, options || {});
this.supportsTouch = this.supportsTouch();
if (this.supportsTouch) {
this.setup(manager);
}
}
setup(manager) {
this.manager = manager;
this.layout = this.manager.layout;
this.fullsize = this.manager.settings.fullsize;
if (this.fullsize) {
this.element = this.manager.stage.element;
this.scroller = window;
this.disableScroll();
} else {
this.element = this.manager.stage.container;
this.scroller = this.element;
this.element.style["WebkitOverflowScrolling"] = "touch";
}
// this.overflow = this.manager.overflow;
// set lookahead offset to page width
this.manager.settings.offset = this.layout.width;
this.manager.settings.afterScrolledTimeout = this.settings.duration * 2;
this.isVertical = this.manager.settings.axis === "vertical";
// disable snapping if not paginated or axis in not horizontal
if (!this.manager.isPaginated || this.isVertical) {
return;
}
this.touchCanceler = false;
this.resizeCanceler = false;
this.snapping = false;
this.scrollLeft;
this.scrollTop;
this.startTouchX = undefined;
this.startTouchY = undefined;
this.startTime = undefined;
this.endTouchX = undefined;
this.endTouchY = undefined;
this.endTime = undefined;
this.addListeners();
}
supportsTouch() {
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
return true;
}
return false;
}
disableScroll() {
this.element.style.overflow = "hidden";
}
enableScroll() {
this.element.style.overflow = "";
}
addListeners() {
this._onResize = this.onResize.bind(this);
window.addEventListener('resize', this._onResize);
this._onScroll = this.onScroll.bind(this);
this.scroller.addEventListener('scroll', this._onScroll);
this._onTouchStart = this.onTouchStart.bind(this);
this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true });
this.on('touchstart', this._onTouchStart);
this._onTouchMove = this.onTouchMove.bind(this);
this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true });
this.on('touchmove', this._onTouchMove);
this._onTouchEnd = this.onTouchEnd.bind(this);
this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true });
this.on('touchend', this._onTouchEnd);
this._afterDisplayed = this.afterDisplayed.bind(this);
this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
}
removeListeners() {
window.removeEventListener('resize', this._onResize);
this._onResize = undefined;
this.scroller.removeEventListener('scroll', this._onScroll);
this._onScroll = undefined;
this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true });
this.off('touchstart', this._onTouchStart);
this._onTouchStart = undefined;
this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true });
this.off('touchmove', this._onTouchMove);
this._onTouchMove = undefined;
this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true });
this.off('touchend', this._onTouchEnd);
this._onTouchEnd = undefined;
this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
this._afterDisplayed = undefined;
}
afterDisplayed(view) {
let contents = view.contents;
["touchstart", "touchmove", "touchend"].forEach((e) => {
contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
});
}
triggerViewEvent(e, contents){
this.emit(e.type, e, contents);
}
onScroll(e) {
this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft;
this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop;
}
onResize(e) {
this.resizeCanceler = true;
}
onTouchStart(e) {
let { screenX, screenY } = e.touches[0];
if (this.fullsize) {
this.enableScroll();
}
this.touchCanceler = true;
if (!this.startTouchX) {
this.startTouchX = screenX;
this.startTouchY = screenY;
this.startTime = this.now();
}
this.endTouchX = screenX;
this.endTouchY = screenY;
this.endTime = this.now();
}
onTouchMove(e) {
let { screenX, screenY } = e.touches[0];
let deltaY = Math.abs(screenY - this.endTouchY);
this.touchCanceler = true;
if (!this.fullsize && deltaY < 10) {
this.element.scrollLeft -= screenX - this.endTouchX;
}
this.endTouchX = screenX;
this.endTouchY = screenY;
this.endTime = this.now();
}
onTouchEnd(e) {
if (this.fullsize) {
this.disableScroll();
}
this.touchCanceler = false;
let swipped = this.wasSwiped();
if (swipped !== 0) {
this.snap(swipped);
} else {
this.snap();
}
this.startTouchX = undefined;
this.startTouchY = undefined;
this.startTime = undefined;
this.endTouchX = undefined;
this.endTouchY = undefined;
this.endTime = undefined;
}
wasSwiped() {
let snapWidth = this.layout.pageWidth * this.layout.divisor;
let distance = (this.endTouchX - this.startTouchX);
let absolute = Math.abs(distance);
let time = this.endTime - this.startTime;
let velocity = (distance / time);
let minVelocity = this.settings.minVelocity;
if (absolute <= this.settings.minDistance || absolute >= snapWidth) {
return 0;
}
if (velocity > minVelocity) {
// previous
return -1;
} else if (velocity < -minVelocity) {
// next
return 1;
}
}
needsSnap() {
let left = this.scrollLeft;
let snapWidth = this.layout.pageWidth * this.layout.divisor;
return (left % snapWidth) !== 0;
}
snap(howMany=0) {
let left = this.scrollLeft;
let snapWidth = this.layout.pageWidth * this.layout.divisor;
let snapTo = Math.round(left / snapWidth) * snapWidth;
if (howMany) {
snapTo += (howMany * snapWidth);
}
return this.smoothScrollTo(snapTo);
}
smoothScrollTo(destination) {
const deferred = new defer();
const start = this.scrollLeft;
const startTime = this.now();
const duration = this.settings.duration;
const easing = this.settings.easing;
this.snapping = true;
// add animation loop
function tick() {
const now = this.now();
const time = Math.min(1, ((now - startTime) / duration));
const timeFunction = easing(time);
if (this.touchCanceler || this.resizeCanceler) {
this.resizeCanceler = false;
this.snapping = false;
deferred.resolve();
return;
}
if (time < 1) {
window.requestAnimationFrame(tick.bind(this));
this.scrollTo(start + ((destination - start) * time), 0);
} else {
this.scrollTo(destination, 0);
this.snapping = false;
deferred.resolve();
}
}
tick.call(this);
return deferred.promise;
}
scrollTo(left=0, top=0) {
if (this.fullsize) {
window.scroll(left, top);
} else {
this.scroller.scrollLeft = left;
this.scroller.scrollTop = top;
}
}
now() {
return ('now' in window.performance) ? performance.now() : new Date().getTime();
}
destroy() {
if (!this.scroller) {
return;
}
if (this.fullsize) {
this.enableScroll();
}
this.removeListeners();
this.scroller = undefined;
}
}
Object.assign(Snap.prototype, EventEmitter.prototype);
export default Snap;

View file

@ -0,0 +1,363 @@
import {uuid, isNumber, isElement, windowBounds, extend} from "../../utils/core";
import throttle from 'lodash/throttle'
class Stage {
constructor(_options) {
this.settings = _options || {};
this.id = "epubjs-container-" + uuid();
this.container = this.create(this.settings);
if(this.settings.hidden) {
this.wrapper = this.wrap(this.container);
}
}
/*
* Creates an element to render to.
* Resizes to passed width and height or to the elements size
*/
create(options){
let height = options.height;// !== false ? options.height : "100%";
let width = options.width;// !== false ? options.width : "100%";
let overflow = options.overflow || false;
let axis = options.axis || "vertical";
let direction = options.direction;
extend(this.settings, options);
if(options.height && isNumber(options.height)) {
height = options.height + "px";
}
if(options.width && isNumber(options.width)) {
width = options.width + "px";
}
// Create new container element
let container = document.createElement("div");
container.id = this.id;
container.classList.add("epub-container");
// Style Element
// container.style.fontSize = "0";
container.style.wordSpacing = "0";
container.style.lineHeight = "0";
container.style.verticalAlign = "top";
container.style.position = "relative";
if(axis === "horizontal") {
// container.style.whiteSpace = "nowrap";
container.style.display = "flex";
container.style.flexDirection = "row";
container.style.flexWrap = "nowrap";
}
if(width){
container.style.width = width;
}
if(height){
container.style.height = height;
}
if (overflow) {
if (overflow === "scroll" && axis === "vertical") {
container.style["overflow-y"] = overflow;
container.style["overflow-x"] = "hidden";
} else if (overflow === "scroll" && axis === "horizontal") {
container.style["overflow-y"] = "hidden";
container.style["overflow-x"] = overflow;
} else {
container.style["overflow"] = overflow;
}
}
if (direction) {
container.dir = direction;
container.style["direction"] = direction;
}
if (direction && this.settings.fullsize) {
document.body.style["direction"] = direction;
}
return container;
}
wrap(container) {
var wrapper = document.createElement("div");
wrapper.style.visibility = "hidden";
wrapper.style.overflow = "hidden";
wrapper.style.width = "0";
wrapper.style.height = "0";
wrapper.appendChild(container);
return wrapper;
}
getElement(_element){
var element;
if(isElement(_element)) {
element = _element;
} else if (typeof _element === "string") {
element = document.getElementById(_element);
}
if(!element){
throw new Error("Not an Element");
}
return element;
}
attachTo(what){
var element = this.getElement(what);
var base;
if(!element){
return;
}
if(this.settings.hidden) {
base = this.wrapper;
} else {
base = this.container;
}
element.appendChild(base);
this.element = element;
return element;
}
getContainer() {
return this.container;
}
onResize(func){
// Only listen to window for resize event if width and height are not fixed.
// This applies if it is set to a percent or auto.
if(!isNumber(this.settings.width) ||
!isNumber(this.settings.height) ) {
this.resizeFunc = throttle(func, 50);
window.addEventListener("resize", this.resizeFunc, false);
}
}
onOrientationChange(func){
this.orientationChangeFunc = func;
window.addEventListener("orientationchange", this.orientationChangeFunc, false);
}
size(width, height){
var bounds;
let _width = width || this.settings.width;
let _height = height || this.settings.height;
// If width or height are set to false, inherit them from containing element
if(width === null) {
bounds = this.element.getBoundingClientRect();
if(bounds.width) {
width = Math.floor(bounds.width);
this.container.style.width = width + "px";
}
} else {
if (isNumber(width)) {
this.container.style.width = width + "px";
} else {
this.container.style.width = width;
}
}
if(height === null) {
bounds = bounds || this.element.getBoundingClientRect();
if(bounds.height) {
height = bounds.height;
this.container.style.height = height + "px";
}
} else {
if (isNumber(height)) {
this.container.style.height = height + "px";
} else {
this.container.style.height = height;
}
}
if(!isNumber(width)) {
width = this.container.clientWidth;
}
if(!isNumber(height)) {
height = this.container.clientHeight;
}
this.containerStyles = window.getComputedStyle(this.container);
this.containerPadding = {
left: parseFloat(this.containerStyles["padding-left"]) || 0,
right: parseFloat(this.containerStyles["padding-right"]) || 0,
top: parseFloat(this.containerStyles["padding-top"]) || 0,
bottom: parseFloat(this.containerStyles["padding-bottom"]) || 0
};
// Bounds not set, get them from window
let _windowBounds = windowBounds();
let bodyStyles = window.getComputedStyle(document.body);
let bodyPadding = {
left: parseFloat(bodyStyles["padding-left"]) || 0,
right: parseFloat(bodyStyles["padding-right"]) || 0,
top: parseFloat(bodyStyles["padding-top"]) || 0,
bottom: parseFloat(bodyStyles["padding-bottom"]) || 0
};
if (!_width) {
width = _windowBounds.width -
bodyPadding.left -
bodyPadding.right;
}
if ((this.settings.fullsize && !_height) || !_height) {
height = _windowBounds.height -
bodyPadding.top -
bodyPadding.bottom;
}
return {
width: width -
this.containerPadding.left -
this.containerPadding.right,
height: height -
this.containerPadding.top -
this.containerPadding.bottom
};
}
bounds(){
let box;
if (this.container.style.overflow !== "visible") {
box = this.container && this.container.getBoundingClientRect();
}
if(!box || !box.width || !box.height) {
return windowBounds();
} else {
return box;
}
}
getSheet(){
var style = document.createElement("style");
// WebKit hack --> https://davidwalsh.name/add-rules-stylesheets
style.appendChild(document.createTextNode(""));
document.head.appendChild(style);
return style.sheet;
}
addStyleRules(selector, rulesArray){
var scope = "#" + this.id + " ";
var rules = "";
if(!this.sheet){
this.sheet = this.getSheet();
}
rulesArray.forEach(function(set) {
for (var prop in set) {
if(set.hasOwnProperty(prop)) {
rules += prop + ":" + set[prop] + ";";
}
}
});
this.sheet.insertRule(scope + selector + " {" + rules + "}", 0);
}
axis(axis) {
if(axis === "horizontal") {
this.container.style.display = "flex";
this.container.style.flexDirection = "row";
this.container.style.flexWrap = "nowrap";
} else {
this.container.style.display = "block";
}
this.settings.axis = axis;
}
// orientation(orientation) {
// if (orientation === "landscape") {
//
// } else {
//
// }
//
// this.orientation = orientation;
// }
direction(dir) {
if (this.container) {
this.container.dir = dir;
this.container.style["direction"] = dir;
}
if (this.settings.fullsize) {
document.body.style["direction"] = dir;
}
this.settings.dir = dir;
}
overflow(overflow) {
if (this.container) {
if (overflow === "scroll" && this.settings.axis === "vertical") {
this.container.style["overflow-y"] = overflow;
this.container.style["overflow-x"] = "hidden";
} else if (overflow === "scroll" && this.settings.axis === "horizontal") {
this.container.style["overflow-y"] = "hidden";
this.container.style["overflow-x"] = overflow;
} else {
this.container.style["overflow"] = overflow;
}
}
this.settings.overflow = overflow;
}
destroy() {
var base;
if (this.element) {
if(this.settings.hidden) {
base = this.wrapper;
} else {
base = this.container;
}
if(this.element.contains(this.container)) {
this.element.removeChild(this.container);
}
window.removeEventListener("resize", this.resizeFunc);
window.removeEventListener("orientationChange", this.orientationChangeFunc);
}
}
}
export default Stage;

View file

@ -0,0 +1,167 @@
class Views {
constructor(container) {
this.container = container;
this._views = [];
this.length = 0;
this.hidden = false;
}
all() {
return this._views;
}
first() {
return this._views[0];
}
last() {
return this._views[this._views.length-1];
}
indexOf(view) {
return this._views.indexOf(view);
}
slice() {
return this._views.slice.apply(this._views, arguments);
}
get(i) {
return this._views[i];
}
append(view){
this._views.push(view);
if(this.container){
this.container.appendChild(view.element);
}
this.length++;
return view;
}
prepend(view){
this._views.unshift(view);
if(this.container){
this.container.insertBefore(view.element, this.container.firstChild);
}
this.length++;
return view;
}
insert(view, index) {
this._views.splice(index, 0, view);
if(this.container){
if(index < this.container.children.length){
this.container.insertBefore(view.element, this.container.children[index]);
} else {
this.container.appendChild(view.element);
}
}
this.length++;
return view;
}
remove(view) {
var index = this._views.indexOf(view);
if(index > -1) {
this._views.splice(index, 1);
}
this.destroy(view);
this.length--;
}
destroy(view) {
if(view.displayed){
view.destroy();
}
if(this.container){
this.container.removeChild(view.element);
}
view = null;
}
// Iterators
forEach() {
return this._views.forEach.apply(this._views, arguments);
}
clear(){
// Remove all views
var view;
var len = this.length;
if(!this.length) return;
for (var i = 0; i < len; i++) {
view = this._views[i];
this.destroy(view);
}
this._views = [];
this.length = 0;
}
find(section){
var view;
var len = this.length;
for (var i = 0; i < len; i++) {
view = this._views[i];
if(view.displayed && view.section.index == section.index) {
return view;
}
}
}
displayed(){
var displayed = [];
var view;
var len = this.length;
for (var i = 0; i < len; i++) {
view = this._views[i];
if(view.displayed){
displayed.push(view);
}
}
return displayed;
}
show(){
var view;
var len = this.length;
for (var i = 0; i < len; i++) {
view = this._views[i];
if(view.displayed){
view.show();
}
}
this.hidden = false;
}
hide(){
var view;
var len = this.length;
for (var i = 0; i < len; i++) {
view = this._views[i];
if(view.displayed){
view.hide();
}
}
this.hidden = true;
}
}
export default Views;

View file

@ -0,0 +1,852 @@
import { EventEmitter } from "events";
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";
class IframeView extends EventEmitter {
constructor(section, options) {
super();
this.settings = extend({
ignoreClass : "",
axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal",
direction: undefined,
width: 0,
height: 0,
layout: undefined,
globalLayoutProperties: {},
method: undefined,
forceRight: false,
allowScriptedContent: false,
allowPopups: false
}, options || {});
this.id = "epubjs-view-" + uuid();
this.section = section;
this.index = section.index;
this.element = this.container(this.settings.axis);
this.added = false;
this.displayed = false;
this.rendered = false;
// this.width = this.settings.width;
// this.height = this.settings.height;
this.fixedWidth = 0;
this.fixedHeight = 0;
// Blank Cfi for Parsing
this.epubcfi = new EpubCFI();
this.layout = this.settings.layout;
// Dom events to listen for
// this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
this.pane = undefined;
this.highlights = {};
this.underlines = {};
this.marks = {};
}
container(axis) {
var element = document.createElement("div");
element.classList.add("epub-view");
// this.element.style.minHeight = "100px";
element.style.height = "0px";
element.style.width = "0px";
element.style.overflow = "hidden";
element.style.position = "relative";
element.style.display = "block";
if(axis && axis == "horizontal"){
element.style.flex = "none";
} else {
element.style.flex = "initial";
}
return element;
}
create() {
if(this.iframe) {
return this.iframe;
}
if(!this.element) {
this.element = this.createContainer();
}
this.iframe = document.createElement("iframe");
this.iframe.id = this.id;
this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations
this.iframe.style.overflow = "hidden";
this.iframe.seamless = "seamless";
// Back up if seamless isn't supported
this.iframe.style.border = "none";
// sandbox
this.iframe.sandbox = "allow-same-origin";
if (this.settings.allowScriptedContent) {
this.iframe.sandbox += " allow-scripts";
}
if (this.settings.allowPopups) {
this.iframe.sandbox += " allow-popups";
}
this.iframe.setAttribute("enable-annotation", "true");
this.resizing = true;
// this.iframe.style.display = "none";
this.element.style.visibility = "hidden";
this.iframe.style.visibility = "hidden";
this.iframe.style.width = "0";
this.iframe.style.height = "0";
this._width = 0;
this._height = 0;
this.element.setAttribute("ref", this.index);
this.added = true;
this.elementBounds = bounds(this.element);
// if(width || height){
// this.resize(width, height);
// } else if(this.width && this.height){
// this.resize(this.width, this.height);
// } else {
// this.iframeBounds = bounds(this.iframe);
// }
if(("srcdoc" in this.iframe)) {
this.supportsSrcdoc = true;
} else {
this.supportsSrcdoc = false;
}
if (!this.settings.method) {
this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write";
}
return this.iframe;
}
render(request, show) {
// 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);
}
// Render Chain
return this.sectionRender
.then(function(contents){
return this.load(contents);
}.bind(this))
.then(function(){
// find and report the writingMode axis
let writingMode = this.contents.writingMode();
// Set the axis based on the flow and writing mode
let axis;
if (this.settings.flow === "scrolled") {
axis = (writingMode.indexOf("vertical") === 0) ? "horizontal" : "vertical";
} else {
axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
}
if (writingMode.indexOf("vertical") === 0 && this.settings.flow === "paginated") {
this.layout.delta = this.layout.height;
}
this.setAxis(axis);
this.emit(EVENTS.VIEWS.AXIS, axis);
this.setWritingMode(writingMode);
this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode);
// apply the layout function to the contents
this.layout.format(this.contents, this.section, this.axis);
// Listen for events that require an expansion of the iframe
this.addListeners();
return new Promise((resolve, reject) => {
// Expand the iframe to the full size of the content
this.expand();
if (this.settings.forceRight) {
this.element.style.marginLeft = this.width() + "px";
}
resolve();
});
}.bind(this), function(e){
this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
return new Promise((resolve, reject) => {
reject(e);
});
}.bind(this))
.then(function() {
this.emit(EVENTS.VIEWS.RENDERED, this.section);
}.bind(this));
}
reset () {
if (this.iframe) {
this.iframe.style.width = "0";
this.iframe.style.height = "0";
this._width = 0;
this._height = 0;
this._textWidth = undefined;
this._contentWidth = undefined;
this._textHeight = undefined;
this._contentHeight = undefined;
}
this._needsReframe = true;
}
// Determine locks base on settings
size(_width, _height) {
var width = _width || this.settings.width;
var height = _height || this.settings.height;
if(this.layout.name === "pre-paginated") {
this.lock("both", width, height);
} else if(this.settings.axis === "horizontal") {
this.lock("height", width, height);
} else {
this.lock("width", width, height);
}
this.settings.width = width;
this.settings.height = height;
}
// Lock an axis to element dimensions, taking borders into account
lock(what, width, height) {
var elBorders = borders(this.element);
var iframeBorders;
if(this.iframe) {
iframeBorders = borders(this.iframe);
} else {
iframeBorders = {width: 0, height: 0};
}
if(what == "width" && isNumber(width)){
this.lockedWidth = width - elBorders.width - iframeBorders.width;
// this.resize(this.lockedWidth, width); // width keeps ratio correct
}
if(what == "height" && isNumber(height)){
this.lockedHeight = height - elBorders.height - iframeBorders.height;
// this.resize(width, this.lockedHeight);
}
if(what === "both" &&
isNumber(width) &&
isNumber(height)){
this.lockedWidth = width - elBorders.width - iframeBorders.width;
this.lockedHeight = height - elBorders.height - iframeBorders.height;
// this.resize(this.lockedWidth, this.lockedHeight);
}
if(this.displayed && this.iframe) {
// this.contents.layout();
this.expand();
}
}
// Resize a single axis based on content dimensions
expand(force) {
var width = this.lockedWidth;
var height = this.lockedHeight;
var columns;
var textWidth, textHeight;
if(!this.iframe || this._expanding) return;
this._expanding = true;
if(this.layout.name === "pre-paginated") {
width = this.layout.columnWidth;
height = this.layout.height;
}
// Expand Horizontally
else if(this.settings.axis === "horizontal") {
// Get the width of the text
width = this.contents.textWidth();
if (width % this.layout.pageWidth > 0) {
width = Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth;
}
if (this.settings.forceEvenPages) {
columns = (width / this.layout.pageWidth);
if ( this.layout.divisor > 1 &&
this.layout.name === "reflowable" &&
(columns % 2 > 0)) {
// add a blank page
width += this.layout.pageWidth;
}
}
} // Expand Vertically
else if(this.settings.axis === "vertical") {
height = this.contents.textHeight();
if (this.settings.flow === "paginated" &&
height % this.layout.height > 0) {
height = Math.ceil(height / this.layout.height) * this.layout.height;
}
}
// Only Resize if dimensions have changed or
// if Frame is still hidden, so needs reframing
if(this._needsReframe || width != this._width || height != this._height){
this.reframe(width, height);
}
this._expanding = false;
}
reframe(width, height) {
var size;
if(isNumber(width)){
this.element.style.width = width + "px";
this.iframe.style.width = width + "px";
this._width = width;
}
if(isNumber(height)){
this.element.style.height = height + "px";
this.iframe.style.height = height + "px";
this._height = height;
}
let widthDelta = this.prevBounds ? width - this.prevBounds.width : width;
let heightDelta = this.prevBounds ? height - this.prevBounds.height : height;
size = {
width: width,
height: height,
widthDelta: widthDelta,
heightDelta: heightDelta,
};
this.pane && this.pane.render();
requestAnimationFrame(() => {
let mark;
for (let m in this.marks) {
if (this.marks.hasOwnProperty(m)) {
mark = this.marks[m];
this.placeMark(mark.element, mark.range);
}
}
});
this.onResize(this, size);
this.emit(EVENTS.VIEWS.RESIZED, size);
this.prevBounds = size;
this.elementBounds = bounds(this.element);
}
load(contents) {
var loading = new defer();
var loaded = loading.promise;
if(!this.iframe) {
loading.reject(new Error("No Iframe Available"));
return loaded;
}
this.iframe.onload = function(event) {
this.onLoad(event, loading);
}.bind(this);
if (this.settings.method === "blobUrl") {
this.blobUrl = createBlobUrl(contents, "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) {
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.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);
}
this.contents.on(EVENTS.CONTENTS.EXPAND, () => {
if(this.displayed && this.iframe) {
this.expand();
if (this.contents) {
this.layout.format(this.contents);
}
}
});
this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => {
if(this.displayed && this.iframe) {
this.expand();
if (this.contents) {
this.layout.format(this.contents);
}
}
});
promise.resolve(this.contents);
}
setLayout(layout) {
this.layout = layout;
if (this.contents) {
this.layout.format(this.contents);
this.expand();
}
}
setAxis(axis) {
this.settings.axis = axis;
if(axis == "horizontal"){
this.element.style.flex = "none";
} else {
this.element.style.flex = "initial";
}
this.size();
}
setWritingMode(mode) {
// this.element.style.writingMode = writingMode;
this.writingMode = mode;
}
addListeners() {
//TODO: Add content listeners for expanding
}
removeListeners(layoutFunc) {
//TODO: remove content listeners for expanding
}
display(request) {
var displayed = new defer();
if (!this.displayed) {
this.render(request)
.then(function () {
this.emit(EVENTS.VIEWS.DISPLAYED, this);
this.onDisplayed(this);
this.displayed = true;
displayed.resolve(this);
}.bind(this), function (err) {
displayed.reject(err, this);
});
} else {
displayed.resolve(this);
}
return displayed.promise;
}
show() {
this.element.style.visibility = "visible";
if(this.iframe){
this.iframe.style.visibility = "visible";
// Remind Safari to redraw the iframe
this.iframe.style.transform = "translateZ(0)";
this.iframe.offsetWidth;
this.iframe.style.transform = null;
}
this.emit(EVENTS.VIEWS.SHOWN, this);
}
hide() {
// this.iframe.style.display = "none";
this.element.style.visibility = "hidden";
this.iframe.style.visibility = "hidden";
this.stopExpanding = true;
this.emit(EVENTS.VIEWS.HIDDEN, this);
}
offset() {
return {
top: this.element.offsetTop,
left: this.element.offsetLeft
}
}
width() {
return this._width;
}
height() {
return this._height;
}
position() {
return this.element.getBoundingClientRect();
}
locationOf(target) {
var parentPos = this.iframe.getBoundingClientRect();
var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
return {
"left": targetPos.left,
"top": targetPos.top
};
}
onDisplayed(view) {
// Stub, override with a custom functions
}
onResize(view, e) {
// Stub, override with a custom functions
}
bounds(force) {
if(force || !this.elementBounds) {
this.elementBounds = bounds(this.element);
}
return this.elementBounds;
}
highlight(cfiRange, data={}, cb, className = "epubjs-hl", styles = {}) {
if (!this.contents) {
return;
}
const attributes = Object.assign({"fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
let range = this.contents.range(cfiRange);
let emitter = () => {
this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
};
data["epubcfi"] = cfiRange;
if (!this.pane) {
this.pane = new Pane(this.iframe, this.element);
}
let m = new Highlight(range, className, data, attributes);
let h = this.pane.addMark(m);
this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
h.element.setAttribute("ref", className);
h.element.addEventListener("click", emitter);
h.element.addEventListener("touchstart", emitter);
if (cb) {
h.element.addEventListener("click", cb);
h.element.addEventListener("touchstart", cb);
}
return h;
}
underline(cfiRange, data={}, cb, className = "epubjs-ul", styles = {}) {
if (!this.contents) {
return;
}
const attributes = Object.assign({"stroke": "black", "stroke-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
let range = this.contents.range(cfiRange);
let emitter = () => {
this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
};
data["epubcfi"] = cfiRange;
if (!this.pane) {
this.pane = new Pane(this.iframe, this.element);
}
let m = new Underline(range, className, data, attributes);
let h = this.pane.addMark(m);
this.underlines[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
h.element.setAttribute("ref", className);
h.element.addEventListener("click", emitter);
h.element.addEventListener("touchstart", emitter);
if (cb) {
h.element.addEventListener("click", cb);
h.element.addEventListener("touchstart", cb);
}
return h;
}
mark(cfiRange, data={}, cb) {
if (!this.contents) {
return;
}
if (cfiRange in this.marks) {
let item = this.marks[cfiRange];
return item;
}
let range = this.contents.range(cfiRange);
if (!range) {
return;
}
let container = range.commonAncestorContainer;
let parent = (container.nodeType === 1) ? container : container.parentNode;
let emitter = (e) => {
this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
};
if (range.collapsed && container.nodeType === 1) {
range = new Range();
range.selectNodeContents(container);
} else if (range.collapsed) { // Webkit doesn't like collapsed ranges
range = new Range();
range.selectNodeContents(parent);
}
let mark = this.document.createElement("a");
mark.setAttribute("ref", "epubjs-mk");
mark.style.position = "absolute";
mark.dataset["epubcfi"] = cfiRange;
if (data) {
Object.keys(data).forEach((key) => {
mark.dataset[key] = data[key];
});
}
if (cb) {
mark.addEventListener("click", cb);
mark.addEventListener("touchstart", cb);
}
mark.addEventListener("click", emitter);
mark.addEventListener("touchstart", emitter);
this.placeMark(mark, range);
this.element.appendChild(mark);
this.marks[cfiRange] = { "element": mark, "range": range, "listeners": [emitter, cb] };
return parent;
}
placeMark(element, range) {
let top, right, left;
if(this.layout.name === "pre-paginated" ||
this.settings.axis !== "horizontal") {
let pos = range.getBoundingClientRect();
top = pos.top;
right = pos.right;
} else {
// Element might break columns, so find the left most element
let rects = range.getClientRects();
let rect;
for (var i = 0; i != rects.length; i++) {
rect = rects[i];
if (!left || rect.left < left) {
left = rect.left;
// right = rect.right;
right = Math.ceil(left / this.layout.props.pageWidth) * this.layout.props.pageWidth - (this.layout.gap / 2);
top = rect.top;
}
}
}
element.style.top = `${top}px`;
element.style.left = `${right}px`;
}
unhighlight(cfiRange) {
let item;
if (cfiRange in this.highlights) {
item = this.highlights[cfiRange];
this.pane.removeMark(item.mark);
item.listeners.forEach((l) => {
if (l) {
item.element.removeEventListener("click", l);
item.element.removeEventListener("touchstart", l);
};
});
delete this.highlights[cfiRange];
}
}
ununderline(cfiRange) {
let item;
if (cfiRange in this.underlines) {
item = this.underlines[cfiRange];
this.pane.removeMark(item.mark);
item.listeners.forEach((l) => {
if (l) {
item.element.removeEventListener("click", l);
item.element.removeEventListener("touchstart", l);
};
});
delete this.underlines[cfiRange];
}
}
unmark(cfiRange) {
let item;
if (cfiRange in this.marks) {
item = this.marks[cfiRange];
this.element.removeChild(item.element);
item.listeners.forEach((l) => {
if (l) {
item.element.removeEventListener("click", l);
item.element.removeEventListener("touchstart", l);
};
});
delete this.marks[cfiRange];
}
}
destroy() {
for (let cfiRange in this.highlights) {
this.unhighlight(cfiRange);
}
for (let cfiRange in this.underlines) {
this.ununderline(cfiRange);
}
for (let cfiRange in this.marks) {
this.unmark(cfiRange);
}
if (this.blobUrl) {
revokeBlobUrl(this.blobUrl);
}
if(this.displayed){
this.displayed = false;
this.removeListeners();
this.contents.destroy();
this.stopExpanding = true;
this.element.removeChild(this.iframe);
if (this.pane) {
this.pane.element.remove();
this.pane = undefined;
}
this.iframe = undefined;
this.contents = undefined;
this._textWidth = null;
this._textHeight = null;
this._width = null;
this._height = null;
}
// this.element.style.height = "0px";
// this.element.style.width = "0px";
}
}
Object.assign(IframeView.prototype, EventEmitter.prototype);
export default IframeView;

View file

@ -0,0 +1,432 @@
import EventEmitter from "event-emitter";
import {extend, borders, uuid, isNumber, bounds, defer, qs, parse} from "../../utils/core";
import EpubCFI from "../../epubcfi";
import Contents from "../../contents";
import { EVENTS } from "../../utils/constants";
class InlineView {
constructor(section, options) {
this.settings = extend({
ignoreClass : "",
axis: "vertical",
width: 0,
height: 0,
layout: undefined,
globalLayoutProperties: {},
}, options || {});
this.id = "epubjs-view:" + uuid();
this.section = section;
this.index = section.index;
this.element = this.container(this.settings.axis);
this.added = false;
this.displayed = false;
this.rendered = false;
this.width = this.settings.width;
this.height = this.settings.height;
this.fixedWidth = 0;
this.fixedHeight = 0;
// Blank Cfi for Parsing
this.epubcfi = new EpubCFI();
this.layout = this.settings.layout;
// Dom events to listen for
// this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
}
container(axis) {
var element = document.createElement("div");
element.classList.add("epub-view");
// if(this.settings.axis === "horizontal") {
// element.style.width = "auto";
// element.style.height = "0";
// } else {
// element.style.width = "0";
// element.style.height = "auto";
// }
element.style.overflow = "hidden";
if(axis && axis == "horizontal"){
element.style.display = "inline-block";
} else {
element.style.display = "block";
}
return element;
}
create() {
if(this.frame) {
return this.frame;
}
if(!this.element) {
this.element = this.createContainer();
}
this.frame = document.createElement("div");
this.frame.id = this.id;
this.frame.style.overflow = "hidden";
this.frame.style.wordSpacing = "initial";
this.frame.style.lineHeight = "initial";
this.resizing = true;
// this.frame.style.display = "none";
this.element.style.visibility = "hidden";
this.frame.style.visibility = "hidden";
if(this.settings.axis === "horizontal") {
this.frame.style.width = "auto";
this.frame.style.height = "0";
} else {
this.frame.style.width = "0";
this.frame.style.height = "auto";
}
this._width = 0;
this._height = 0;
this.element.appendChild(this.frame);
this.added = true;
this.elementBounds = bounds(this.element);
return this.frame;
}
render(request, show) {
// view.onLayout = this.layout.format.bind(this.layout);
this.create();
// Fit to size of the container, apply padding
this.size();
// Render Chain
return this.section.render(request)
.then(function(contents){
return this.load(contents);
}.bind(this))
// .then(function(doc){
// return this.hooks.content.trigger(view, this);
// }.bind(this))
.then(function(){
// this.settings.layout.format(view.contents);
// return this.hooks.layout.trigger(view, this);
}.bind(this))
// .then(function(){
// return this.display();
// }.bind(this))
// .then(function(){
// return this.hooks.render.trigger(view, this);
// }.bind(this))
.then(function(){
// apply the layout function to the contents
this.settings.layout.format(this.contents);
// Expand the iframe to the full size of the content
// this.expand();
// Listen for events that require an expansion of the iframe
this.addListeners();
if(show !== false) {
//this.q.enqueue(function(view){
this.show();
//}, view);
}
// this.map = new Map(view, this.layout);
//this.hooks.show.trigger(view, this);
this.emit(EVENTS.VIEWS.RENDERED, this.section);
}.bind(this))
.catch(function(e){
this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
}.bind(this));
}
// Determine locks base on settings
size(_width, _height) {
var width = _width || this.settings.width;
var height = _height || this.settings.height;
if(this.layout.name === "pre-paginated") {
// TODO: check if these are different than the size set in chapter
this.lock("both", width, height);
} else if(this.settings.axis === "horizontal") {
this.lock("height", width, height);
} else {
this.lock("width", width, height);
}
}
// Lock an axis to element dimensions, taking borders into account
lock(what, width, height) {
var elBorders = borders(this.element);
var iframeBorders;
if(this.frame) {
iframeBorders = borders(this.frame);
} else {
iframeBorders = {width: 0, height: 0};
}
if(what == "width" && isNumber(width)){
this.lockedWidth = width - elBorders.width - iframeBorders.width;
this.resize(this.lockedWidth, false); // width keeps ratio correct
}
if(what == "height" && isNumber(height)){
this.lockedHeight = height - elBorders.height - iframeBorders.height;
this.resize(false, this.lockedHeight);
}
if(what === "both" &&
isNumber(width) &&
isNumber(height)){
this.lockedWidth = width - elBorders.width - iframeBorders.width;
this.lockedHeight = height - elBorders.height - iframeBorders.height;
this.resize(this.lockedWidth, this.lockedHeight);
}
}
// Resize a single axis based on content dimensions
expand(force) {
var width = this.lockedWidth;
var height = this.lockedHeight;
var textWidth, textHeight;
if(!this.frame || this._expanding) return;
this._expanding = true;
// Expand Horizontally
if(this.settings.axis === "horizontal") {
width = this.contentWidth(textWidth);
} // Expand Vertically
else if(this.settings.axis === "vertical") {
height = this.contentHeight(textHeight);
}
// Only Resize if dimensions have changed or
// if Frame is still hidden, so needs reframing
if(this._needsReframe || width != this._width || height != this._height){
this.resize(width, height);
}
this._expanding = false;
}
contentWidth(min) {
return this.frame.scrollWidth;
}
contentHeight(min) {
return this.frame.scrollHeight;
}
resize(width, height) {
if(!this.frame) return;
if(isNumber(width)){
this.frame.style.width = width + "px";
this._width = width;
}
if(isNumber(height)){
this.frame.style.height = height + "px";
this._height = height;
}
this.prevBounds = this.elementBounds;
this.elementBounds = bounds(this.element);
let size = {
width: this.elementBounds.width,
height: this.elementBounds.height,
widthDelta: this.elementBounds.width - this.prevBounds.width,
heightDelta: this.elementBounds.height - this.prevBounds.height,
};
this.onResize(this, size);
this.emit(EVENTS.VIEWS.RESIZED, size);
}
load(contents) {
var loading = new defer();
var loaded = loading.promise;
var doc = parse(contents, "text/html");
var body = qs(doc, "body");
/*
var srcs = doc.querySelectorAll("[src]");
Array.prototype.slice.call(srcs)
.forEach(function(item) {
var src = item.getAttribute("src");
var assetUri = URI(src);
var origin = assetUri.origin();
var absoluteUri;
if (!origin) {
absoluteUri = assetUri.absoluteTo(this.section.url);
item.src = absoluteUri;
}
}.bind(this));
*/
this.frame.innerHTML = body.innerHTML;
this.document = this.frame.ownerDocument;
this.window = this.document.defaultView;
this.contents = new Contents(this.document, this.frame);
this.rendering = false;
loading.resolve(this.contents);
return loaded;
}
setLayout(layout) {
this.layout = layout;
}
resizeListenters() {
// Test size again
// clearTimeout(this.expanding);
// this.expanding = setTimeout(this.expand.bind(this), 350);
}
addListeners() {
//TODO: Add content listeners for expanding
}
removeListeners(layoutFunc) {
//TODO: remove content listeners for expanding
}
display(request) {
var displayed = new defer();
if (!this.displayed) {
this.render(request).then(function () {
this.emit(EVENTS.VIEWS.DISPLAYED, this);
this.onDisplayed(this);
this.displayed = true;
displayed.resolve(this);
}.bind(this));
} else {
displayed.resolve(this);
}
return displayed.promise;
}
show() {
this.element.style.visibility = "visible";
if(this.frame){
this.frame.style.visibility = "visible";
}
this.emit(EVENTS.VIEWS.SHOWN, this);
}
hide() {
// this.frame.style.display = "none";
this.element.style.visibility = "hidden";
this.frame.style.visibility = "hidden";
this.stopExpanding = true;
this.emit(EVENTS.VIEWS.HIDDEN, this);
}
position() {
return this.element.getBoundingClientRect();
}
locationOf(target) {
var parentPos = this.frame.getBoundingClientRect();
var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
return {
"left": window.scrollX + parentPos.left + targetPos.left,
"top": window.scrollY + parentPos.top + targetPos.top
};
}
onDisplayed(view) {
// Stub, override with a custom functions
}
onResize(view, e) {
// Stub, override with a custom functions
}
bounds() {
if(!this.elementBounds) {
this.elementBounds = bounds(this.element);
}
return this.elementBounds;
}
destroy() {
if(this.displayed){
this.displayed = false;
this.removeListeners();
this.stopExpanding = true;
this.element.removeChild(this.frame);
this.displayed = false;
this.frame = null;
this._textWidth = null;
this._textHeight = null;
this._width = null;
this._height = null;
}
// this.element.style.height = "0px";
// this.element.style.width = "0px";
}
}
EventEmitter(InlineView.prototype);
export default InlineView;

View file

@ -0,0 +1,511 @@
import EpubCFI from "./epubcfi";
import { nodeBounds } from "./utils/core";
/**
* Map text locations to CFI ranges
* @class
* @param {Layout} layout Layout to apply
* @param {string} [direction="ltr"] Text direction
* @param {string} [axis="horizontal"] vertical or horizontal axis
* @param {boolean} [dev] toggle developer highlighting
*/
class Mapping {
constructor(layout, direction, axis, dev=false) {
this.layout = layout;
this.horizontal = (axis === "horizontal") ? true : false;
this.direction = direction || "ltr";
this._dev = dev;
}
/**
* Find CFI pairs for entire section at once
*/
section(view) {
var ranges = this.findRanges(view);
var map = this.rangeListToCfiList(view.section.cfiBase, ranges);
return map;
}
/**
* Find CFI pairs for a page
* @param {Contents} contents Contents from view
* @param {string} cfiBase string of the base for a cfi
* @param {number} start position to start at
* @param {number} end position to end at
*/
page(contents, cfiBase, start, end) {
var root = contents && contents.document ? contents.document.body : false;
var result;
if (!root) {
return;
}
result = this.rangePairToCfiPair(cfiBase, {
start: this.findStart(root, start, end),
end: this.findEnd(root, start, end)
});
if (this._dev === true) {
let doc = contents.document;
let startRange = new EpubCFI(result.start).toRange(doc);
let endRange = new EpubCFI(result.end).toRange(doc);
let selection = doc.defaultView.getSelection();
let r = doc.createRange();
selection.removeAllRanges();
r.setStart(startRange.startContainer, startRange.startOffset);
r.setEnd(endRange.endContainer, endRange.endOffset);
selection.addRange(r);
}
return result;
}
/**
* Walk a node, preforming a function on each node it finds
* @private
* @param {Node} root Node to walkToNode
* @param {function} func walk function
* @return {*} returns the result of the walk function
*/
walk(root, func) {
// IE11 has strange issue, if root is text node IE throws exception on
// calling treeWalker.nextNode(), saying
// Unexpected call to method or property access instead of returning null value
if(root && root.nodeType === Node.TEXT_NODE) {
return;
}
// safeFilter is required so that it can work in IE as filter is a function for IE
// and for other browser filter is an object.
var filter = {
acceptNode: function(node) {
if (node.data.trim().length > 0) {
return NodeFilter.FILTER_ACCEPT;
} else {
return NodeFilter.FILTER_REJECT;
}
}
};
var safeFilter = filter.acceptNode;
safeFilter.acceptNode = filter.acceptNode;
var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, safeFilter, false);
var node;
var result;
while ((node = treeWalker.nextNode())) {
result = func(node);
if(result) break;
}
return result;
}
findRanges(view){
var columns = [];
var scrollWidth = view.contents.scrollWidth();
var spreads = Math.ceil( scrollWidth / this.layout.spreadWidth);
var count = spreads * this.layout.divisor;
var columnWidth = this.layout.columnWidth;
var gap = this.layout.gap;
var start, end;
for (var i = 0; i < count.pages; i++) {
start = (columnWidth + gap) * i;
end = (columnWidth * (i+1)) + (gap * i);
columns.push({
start: this.findStart(view.document.body, start, end),
end: this.findEnd(view.document.body, start, end)
});
}
return columns;
}
/**
* Find Start Range
* @private
* @param {Node} root root node
* @param {number} start position to start at
* @param {number} end position to end at
* @return {Range}
*/
findStart(root, start, end){
var stack = [root];
var $el;
var found;
var $prev = root;
while (stack.length) {
$el = stack.shift();
found = this.walk($el, (node) => {
var left, right, top, bottom;
var elPos;
var elRange;
elPos = nodeBounds(node);
if (this.horizontal && this.direction === "ltr") {
left = this.horizontal ? elPos.left : elPos.top;
right = this.horizontal ? elPos.right : elPos.bottom;
if( left >= start && left <= end ) {
return node;
} else if (right > start) {
return node;
} else {
$prev = node;
stack.push(node);
}
} else if (this.horizontal && this.direction === "rtl") {
left = elPos.left;
right = elPos.right;
if( right <= end && right >= start ) {
return node;
} else if (left < end) {
return node;
} else {
$prev = node;
stack.push(node);
}
} else {
top = elPos.top;
bottom = elPos.bottom;
if( top >= start && top <= end ) {
return node;
} else if (bottom > start) {
return node;
} else {
$prev = node;
stack.push(node);
}
}
});
if(found) {
return this.findTextStartRange(found, start, end);
}
}
// Return last element
return this.findTextStartRange($prev, start, end);
}
/**
* Find End Range
* @private
* @param {Node} root root node
* @param {number} start position to start at
* @param {number} end position to end at
* @return {Range}
*/
findEnd(root, start, end){
var stack = [root];
var $el;
var $prev = root;
var found;
while (stack.length) {
$el = stack.shift();
found = this.walk($el, (node) => {
var left, right, top, bottom;
var elPos;
var elRange;
elPos = nodeBounds(node);
if (this.horizontal && this.direction === "ltr") {
left = Math.round(elPos.left);
right = Math.round(elPos.right);
if(left > end && $prev) {
return $prev;
} else if(right > end) {
return node;
} else {
$prev = node;
stack.push(node);
}
} else if (this.horizontal && this.direction === "rtl") {
left = Math.round(this.horizontal ? elPos.left : elPos.top);
right = Math.round(this.horizontal ? elPos.right : elPos.bottom);
if(right < start && $prev) {
return $prev;
} else if(left < start) {
return node;
} else {
$prev = node;
stack.push(node);
}
} else {
top = Math.round(elPos.top);
bottom = Math.round(elPos.bottom);
if(top > end && $prev) {
return $prev;
} else if(bottom > end) {
return node;
} else {
$prev = node;
stack.push(node);
}
}
});
if(found){
return this.findTextEndRange(found, start, end);
}
}
// end of chapter
return this.findTextEndRange($prev, start, end);
}
/**
* Find Text Start Range
* @private
* @param {Node} root root node
* @param {number} start position to start at
* @param {number} end position to end at
* @return {Range}
*/
findTextStartRange(node, start, end){
var ranges = this.splitTextNodeIntoRanges(node);
var range;
var pos;
var left, top, right;
for (var i = 0; i < ranges.length; i++) {
range = ranges[i];
pos = range.getBoundingClientRect();
if (this.horizontal && this.direction === "ltr") {
left = pos.left;
if( left >= start ) {
return range;
}
} else if (this.horizontal && this.direction === "rtl") {
right = pos.right;
if( right <= end ) {
return range;
}
} else {
top = pos.top;
if( top >= start ) {
return range;
}
}
// prev = range;
}
return ranges[0];
}
/**
* Find Text End Range
* @private
* @param {Node} root root node
* @param {number} start position to start at
* @param {number} end position to end at
* @return {Range}
*/
findTextEndRange(node, start, end){
var ranges = this.splitTextNodeIntoRanges(node);
var prev;
var range;
var pos;
var left, right, top, bottom;
for (var i = 0; i < ranges.length; i++) {
range = ranges[i];
pos = range.getBoundingClientRect();
if (this.horizontal && this.direction === "ltr") {
left = pos.left;
right = pos.right;
if(left > end && prev) {
return prev;
} else if(right > end) {
return range;
}
} else if (this.horizontal && this.direction === "rtl") {
left = pos.left
right = pos.right;
if(right < start && prev) {
return prev;
} else if(left < start) {
return range;
}
} else {
top = pos.top;
bottom = pos.bottom;
if(top > end && prev) {
return prev;
} else if(bottom > end) {
return range;
}
}
prev = range;
}
// Ends before limit
return ranges[ranges.length-1];
}
/**
* Split up a text node into ranges for each word
* @private
* @param {Node} root root node
* @param {string} [_splitter] what to split on
* @return {Range[]}
*/
splitTextNodeIntoRanges(node, _splitter){
var ranges = [];
var textContent = node.textContent || "";
var text = textContent.trim();
var range;
var doc = node.ownerDocument;
var splitter = _splitter || " ";
var pos = text.indexOf(splitter);
if(pos === -1 || node.nodeType != Node.TEXT_NODE) {
range = doc.createRange();
range.selectNodeContents(node);
return [range];
}
range = doc.createRange();
range.setStart(node, 0);
range.setEnd(node, pos);
ranges.push(range);
range = false;
while ( pos != -1 ) {
pos = text.indexOf(splitter, pos + 1);
if(pos > 0) {
if(range) {
range.setEnd(node, pos);
ranges.push(range);
}
range = doc.createRange();
range.setStart(node, pos+1);
}
}
if(range) {
range.setEnd(node, text.length);
ranges.push(range);
}
return ranges;
}
/**
* Turn a pair of ranges into a pair of CFIs
* @private
* @param {string} cfiBase base string for an EpubCFI
* @param {object} rangePair { start: Range, end: Range }
* @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" }
*/
rangePairToCfiPair(cfiBase, rangePair){
var startRange = rangePair.start;
var endRange = rangePair.end;
startRange.collapse(true);
endRange.collapse(false);
let startCfi = new EpubCFI(startRange, cfiBase).toString();
let endCfi = new EpubCFI(endRange, cfiBase).toString();
return {
start: startCfi,
end: endCfi
};
}
rangeListToCfiList(cfiBase, columns){
var map = [];
var cifPair;
for (var i = 0; i < columns.length; i++) {
cifPair = this.rangePairToCfiPair(cfiBase, columns[i]);
map.push(cifPair);
}
return map;
}
/**
* Set the axis for mapping
* @param {string} axis horizontal | vertical
* @return {boolean} is it horizontal?
*/
axis(axis) {
if (axis) {
this.horizontal = (axis === "horizontal") ? true : false;
}
return this.horizontal;
}
}
export default Mapping;

View file

@ -0,0 +1,357 @@
import {qs, qsa, querySelectorByType, filterChildren, getParentByTagName} from "./utils/core";
/**
* Navigation Parser
* @param {document} xml navigation html / xhtml / ncx
*/
class Navigation {
constructor(xml) {
this.toc = [];
this.tocByHref = {};
this.tocById = {};
this.landmarks = [];
this.landmarksByType = {};
this.length = 0;
if (xml) {
this.parse(xml);
}
}
/**
* Parse out the navigation items
* @param {document} xml navigation html / xhtml / ncx
*/
parse(xml) {
let isXml = xml.nodeType;
let html;
let ncx;
if (isXml) {
html = qs(xml, "html");
ncx = qs(xml, "ncx");
}
if (!isXml) {
this.toc = this.load(xml);
} else if(html) {
this.toc = this.parseNav(xml);
this.landmarks = this.parseLandmarks(xml);
} else if(ncx){
this.toc = this.parseNcx(xml);
}
this.length = 0;
this.unpack(this.toc);
}
/**
* Unpack navigation items
* @private
* @param {array} toc
*/
unpack(toc) {
var item;
for (var i = 0; i < toc.length; i++) {
item = toc[i];
if (item.href) {
this.tocByHref[item.href] = i;
}
if (item.id) {
this.tocById[item.id] = i;
}
this.length++;
if (item.subitems.length) {
this.unpack(item.subitems);
}
}
}
/**
* Get an item from the navigation
* @param {string} target
* @return {object} navItem
*/
get(target) {
var index;
if(!target) {
return this.toc;
}
if(target.indexOf("#") === 0) {
index = this.tocById[target.substring(1)];
} else if(target in this.tocByHref){
index = this.tocByHref[target];
}
return this.getByIndex(target, index, this.toc);
}
/**
* Get an item from navigation subitems recursively by index
* @param {string} target
* @param {number} index
* @param {array} navItems
* @return {object} navItem
*/
getByIndex(target, index, navItems) {
if (navItems.length === 0) {
return;
}
const item = navItems[index];
if (item && (target === item.id || target === item.href)) {
return item;
} else {
let result;
for (let i = 0; i < navItems.length; ++i) {
result = this.getByIndex(target, index, navItems[i].subitems);
if (result) {
break;
}
}
return result;
}
}
/**
* Get a landmark by type
* List of types: https://idpf.github.io/epub-vocabs/structure/
* @param {string} type
* @return {object} landmarkItem
*/
landmark(type) {
var index;
if(!type) {
return this.landmarks;
}
index = this.landmarksByType[type];
return this.landmarks[index];
}
/**
* Parse toc from a Epub > 3.0 Nav
* @private
* @param {document} navHtml
* @return {array} navigation list
*/
parseNav(navHtml){
var navElement = querySelectorByType(navHtml, "nav", "toc");
var list = [];
if (!navElement) return list;
let navList = filterChildren(navElement, "ol", true);
if (!navList) return list;
list = this.parseNavList(navList);
return list;
}
/**
* Parses lists in the toc
* @param {document} navListHtml
* @param {string} parent id
* @return {array} navigation list
*/
parseNavList(navListHtml, parent) {
const result = [];
if (!navListHtml) return result;
if (!navListHtml.children) return result;
for (let i = 0; i < navListHtml.children.length; i++) {
const item = this.navItem(navListHtml.children[i], parent);
if (item) {
result.push(item);
}
}
return result;
}
/**
* Create a navItem
* @private
* @param {element} item
* @return {object} navItem
*/
navItem(item, parent) {
let id = item.getAttribute("id") || undefined;
let content = filterChildren(item, "a", true)
|| filterChildren(item, "span", true);
if (!content) {
return;
}
let src = content.getAttribute("href") || "";
if (!id) {
id = src;
}
let text = content.textContent || "";
let subitems = [];
let nested = filterChildren(item, "ol", true);
if (nested) {
subitems = this.parseNavList(nested, id);
}
return {
"id": id,
"href": src,
"label": text,
"subitems" : subitems,
"parent" : parent
};
}
/**
* Parse landmarks from a Epub > 3.0 Nav
* @private
* @param {document} navHtml
* @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;
if(!navItems || length === 0) return list;
for (i = 0; i < length; ++i) {
item = this.landmarkItem(navItems[i]);
if (item) {
list.push(item);
this.landmarksByType[item.type] = i;
}
}
return list;
}
/**
* Create a landmarkItem
* @private
* @param {element} item
* @return {object} landmarkItem
*/
landmarkItem(item){
let content = filterChildren(item, "a", true);
if (!content) {
return;
}
let type = content.getAttributeNS("http://www.idpf.org/2007/ops", "type") || undefined;
let href = content.getAttribute("href") || "";
let text = content.textContent || "";
return {
"href": href,
"label": text,
"type" : type
};
}
/**
* Parse from a Epub > 3.0 NC
* @private
* @param {document} navHtml
* @return {array} navigation list
*/
parseNcx(tocXml){
var navPoints = qsa(tocXml, "navPoint");
var length = navPoints.length;
var i;
var toc = {};
var list = [];
var item, parent;
if(!navPoints || length === 0) return list;
for (i = 0; i < length; ++i) {
item = this.ncxItem(navPoints[i]);
toc[item.id] = item;
if(!item.parent) {
list.push(item);
} else {
parent = toc[item.parent];
parent.subitems.push(item);
}
}
return list;
}
/**
* Create a ncxItem
* @private
* @param {element} item
* @return {object} ncxItem
*/
ncxItem(item){
var id = item.getAttribute("id") || false,
content = qs(item, "content"),
src = content.getAttribute("src"),
navLabel = qs(item, "navLabel"),
text = navLabel.textContent ? navLabel.textContent : "",
subitems = [],
parentNode = item.parentNode,
parent;
if(parentNode && (parentNode.nodeName === "navPoint" || parentNode.nodeName.split(':').slice(-1)[0] === "navPoint")) {
parent = parentNode.getAttribute("id");
}
return {
"id": id,
"href": src,
"label": text,
"subitems" : subitems,
"parent" : 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
* @return {method} forEach loop
*/
forEach(fn) {
return this.toc.forEach(fn);
}
}
export default Navigation;

View file

@ -0,0 +1,373 @@
import {qs, qsa, qsp, indexOfElementNode} from "./utils/core";
/**
* Open Packaging Format Parser
* @class
* @param {document} packageDocument OPF XML
*/
class Packaging {
constructor(packageDocument) {
this.manifest = {};
this.navPath = '';
this.ncxPath = '';
this.coverPath = '';
this.spineNodeIndex = 0;
this.spine = [];
this.metadata = {};
if (packageDocument) {
this.parse(packageDocument);
}
}
/**
* Parse OPF XML
* @param {document} packageDocument OPF XML
* @return {object} parsed package parts
*/
parse(packageDocument){
var metadataNode, manifestNode, spineNode;
if(!packageDocument) {
throw new Error("Package File Not Found");
}
metadataNode = qs(packageDocument, "metadata");
if(!metadataNode) {
throw new Error("No Metadata Found");
}
manifestNode = qs(packageDocument, "manifest");
if(!manifestNode) {
throw new Error("No Manifest Found");
}
spineNode = qs(packageDocument, "spine");
if(!spineNode) {
throw new Error("No Spine Found");
}
this.manifest = this.parseManifest(manifestNode);
this.navPath = this.findNavPath(manifestNode);
this.ncxPath = this.findNcxPath(manifestNode, spineNode);
this.coverPath = this.findCoverPath(packageDocument);
this.spineNodeIndex = indexOfElementNode(spineNode);
this.spine = this.parseSpine(spineNode, this.manifest);
this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument);
this.metadata = this.parseMetadata(metadataNode);
this.metadata.direction = spineNode.getAttribute("page-progression-direction");
return {
"metadata" : this.metadata,
"spine" : this.spine,
"manifest" : this.manifest,
"navPath" : this.navPath,
"ncxPath" : this.ncxPath,
"coverPath": this.coverPath,
"spineNodeIndex" : this.spineNodeIndex
};
}
/**
* Parse Metadata
* @private
* @param {node} xml
* @return {object} metadata
*/
parseMetadata(xml){
var metadata = {};
metadata.title = this.getElementText(xml, "title");
metadata.creator = this.getElementText(xml, "creator");
metadata.description = this.getElementText(xml, "description");
metadata.pubdate = this.getElementText(xml, "date");
metadata.publisher = this.getElementText(xml, "publisher");
metadata.identifier = this.getElementText(xml, "identifier");
metadata.language = this.getElementText(xml, "language");
metadata.rights = this.getElementText(xml, "rights");
metadata.modified_date = this.getPropertyText(xml, "dcterms:modified");
metadata.layout = this.getPropertyText(xml, "rendition:layout");
metadata.orientation = this.getPropertyText(xml, "rendition:orientation");
metadata.flow = this.getPropertyText(xml, "rendition:flow");
metadata.viewport = this.getPropertyText(xml, "rendition:viewport");
metadata.media_active_class = this.getPropertyText(xml, "media:active-class");
metadata.spread = this.getPropertyText(xml, "rendition:spread");
// metadata.page_prog_dir = packageXml.querySelector("spine").getAttribute("page-progression-direction");
return metadata;
}
/**
* Parse Manifest
* @private
* @param {node} manifestXml
* @return {object} manifest
*/
parseManifest(manifestXml){
var manifest = {};
//-- Turn items into an array
// var selected = manifestXml.querySelectorAll("item");
var selected = qsa(manifestXml, "item");
var items = Array.prototype.slice.call(selected);
//-- Create an object with the id as key
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") || "";
manifest[id] = {
"href" : href,
// "url" : href,
"type" : type,
"overlay" : overlay,
"properties" : properties.length ? properties.split(" ") : []
};
});
return manifest;
}
/**
* Parse Spine
* @private
* @param {node} spineXml
* @param {Packaging.manifest} manifest
* @return {object} spine
*/
parseSpine(spineXml, manifest){
var spine = [];
var selected = qsa(spineXml, "itemref");
var 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(" ") : [];
var itemref = {
"id" : item.getAttribute("id"),
"idref" : idref,
"linear" : item.getAttribute("linear") || "yes",
"properties" : propArray,
// "href" : manifest[Id].href,
// "url" : manifest[Id].url,
"index" : index
// "cfiBase" : cfiBase
};
spine.push(itemref);
});
return spine;
}
/**
* Find Unique Identifier
* @private
* @param {node} packageXml
* @return {string} Unique Identifier text
*/
findUniqueIdentifier(packageXml){
var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier");
if (! uniqueIdentifierId) {
return "";
}
var identifier = packageXml.getElementById(uniqueIdentifierId);
if (! identifier) {
return "";
}
if (identifier.localName === "identifier" && identifier.namespaceURI === "http://purl.org/dc/elements/1.1/") {
return identifier.childNodes.length > 0 ? identifier.childNodes[0].nodeValue.trim() : "";
}
return "";
}
/**
* Find TOC NAV
* @private
* @param {element} manifestNode
* @return {string}
*/
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"});
return node ? node.getAttribute("href") : false;
}
/**
* Find TOC NCX
* media-type="application/x-dtbncx+xml" href="toc.ncx"
* @private
* @param {element} manifestNode
* @param {element} spineNode
* @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;
// 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");
if(tocId) {
// node = manifestNode.querySelector("item[id='" + tocId + "']");
node = manifestNode.querySelector(`#${tocId}`);
}
}
return node ? node.getAttribute("href") : false;
}
/**
* Find the Cover Path
* <item properties="cover-image" id="ci" href="cover.svg" media-type="image/svg+xml" />
* Fallback for Epub 2.0
* @private
* @param {node} packageXml
* @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");
// Fallback to epub 2.
var 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);
return cover ? cover.getAttribute("href") : "";
}
else {
return false;
}
}
/**
* Get text of a namespaced element
* @private
* @param {node} xml
* @param {string} tag
* @return {string} text
*/
getElementText(xml, tag){
var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag);
var el;
if(!found || found.length === 0) return "";
el = found[0];
if(el.childNodes.length){
return el.childNodes[0].nodeValue;
}
return "";
}
/**
* Get text by property
* @private
* @param {node} xml
* @param {string} property
* @return {string} text
*/
getPropertyText(xml, property){
var el = qsp(xml, "meta", {"property":property});
if(el && el.childNodes.length){
return el.childNodes[0].nodeValue;
}
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;
this.ncxPath = undefined;
this.coverPath = undefined;
this.spineNodeIndex = undefined;
this.spine = undefined;
this.metadata = undefined;
}
}
export default Packaging;

View file

@ -0,0 +1,274 @@
import EpubCFI from "./epubcfi";
import {
qs,
qsa,
querySelectorByType,
indexOfSorted,
locationOf
} from "./utils/core";
/**
* Page List Parser
* @param {document} [xml]
*/
class PageList {
constructor(xml) {
this.pages = [];
this.locations = [];
this.epubcfi = new EpubCFI();
this.firstPage = 0;
this.lastPage = 0;
this.totalPages = 0;
this.toc = undefined;
this.ncx = undefined;
if (xml) {
this.pageList = this.parse(xml);
}
if(this.pageList && this.pageList.length) {
this.process(this.pageList);
}
}
/**
* Parse PageList Xml
* @param {document} xml
*/
parse(xml) {
var html = qs(xml, "html");
var ncx = qs(xml, "ncx");
if(html) {
return this.parseNav(xml);
} else if(ncx){
return this.parseNcx(xml);
}
}
/**
* Parse a Nav PageList
* @private
* @param {node} navHtml
* @return {PageList.item[]} list
*/
parseNav(navHtml){
var navElement = querySelectorByType(navHtml, "nav", "page-list");
var navItems = navElement ? qsa(navElement, "li") : [];
var length = navItems.length;
var i;
var list = [];
var item;
if(!navItems || length === 0) return list;
for (i = 0; i < length; ++i) {
item = this.item(navItems[i]);
list.push(item);
}
return list;
}
parseNcx(navXml) {
var list = [];
var i = 0;
var item;
var pageList;
var pageTargets;
var length = 0;
pageList = qs(navXml, "pageList");
if (!pageList) return list;
pageTargets = qsa(pageList, "pageTarget");
length = pageTargets.length;
if (!pageTargets || pageTargets.length === 0) {
return list;
}
for (i = 0; i < length; ++i) {
item = this.ncxItem(pageTargets[i]);
list.push(item);
}
return list;
}
ncxItem(item) {
var navLabel = qs(item, "navLabel");
var navLabelText = qs(navLabel, "text");
var pageText = navLabelText.textContent;
var content = qs(item, "content");
var href = content.getAttribute("src");
var page = parseInt(pageText, 10);
return {
"href": href,
"page": page,
};
}
/**
* Page List Item
* @private
* @param {node} item
* @return {object} pageListItem
*/
item(item){
var content = qs(item, "a"),
href = content.getAttribute("href") || "",
text = content.textContent || "",
page = parseInt(text),
isCfi = href.indexOf("epubcfi"),
split,
packageUrl,
cfi;
if(isCfi != -1) {
split = href.split("#");
packageUrl = split[0];
cfi = split.length > 1 ? split[1] : false;
return {
"cfi" : cfi,
"href" : href,
"packageUrl" : packageUrl,
"page" : page
};
} else {
return {
"href" : href,
"page" : page
};
}
}
/**
* Process pageList items
* @private
* @param {array} pageList
*/
process(pageList){
pageList.forEach(function(item){
this.pages.push(item.page);
if (item.cfi) {
this.locations.push(item.cfi);
}
}, this);
this.firstPage = parseInt(this.pages[0]);
this.lastPage = parseInt(this.pages[this.pages.length-1]);
this.totalPages = this.lastPage - this.firstPage;
}
/**
* Get a PageList result from a EpubCFI
* @param {string} cfi EpubCFI String
* @return {number} page
*/
pageFromCfi(cfi){
var pg = -1;
// Check if the pageList has not been set yet
if(this.locations.length === 0) {
return -1;
}
// TODO: check if CFI is valid?
// check if the cfi is in the location list
// var index = this.locations.indexOf(cfi);
var index = indexOfSorted(cfi, this.locations, this.epubcfi.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 = EPUBJS.core.insert(cfi, this.locations, this.epubcfi.compare);
index = locationOf(cfi, this.locations, this.epubcfi.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 | number} 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.locations[index];
}
// TODO: handle pages not in the list
return cfi;
}
/**
* Get a Page from Book percentage
* @param {number} percent
* @return {number} 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 {number} 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
*/
percentageFromCfi(cfi){
var pg = this.pageFromCfi(cfi);
var percentage = this.percentageFromPage(pg);
return percentage;
}
/**
* Destroy
*/
destroy() {
this.pages = undefined;
this.locations = undefined;
this.epubcfi = undefined;
this.pageList = undefined;
this.toc = undefined;
this.ncx = undefined;
}
}
export default PageList;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,320 @@
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;

View file

@ -0,0 +1,323 @@
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";
/**
* Represents a Section of the Book
*
* In most books this is equivalent to a Chapter
* @param {object} item The spine item representing the section
* @param {object} hooks hooks for serialize and content
*/
class Section {
constructor(item, hooks){
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.canonical = item.canonical;
this.next = item.next;
this.prev = item.prev;
this.cfiBase = item.cfiBase;
if (hooks) {
this.hooks = hooks;
} else {
this.hooks = {};
this.hooks.serialize = new Hook(this);
this.hooks.content = new Hook(this);
}
this.document = undefined;
this.contents = undefined;
this.output = undefined;
}
/**
* Load the section from its url
* @param {method} [_request] a request method to use for loading
* @return {document} a promise with the xml document
*/
load(_request){
var request = _request || this.request || Request;
var loading = new defer();
var loaded = loading.promise;
if(this.contents) {
loading.resolve(this.contents);
} else {
request(this.url)
.then(function(xml){
// var directory = new Url(this.url).directory;
this.document = xml;
this.contents = xml.documentElement;
return this.hooks.content.trigger(this.document, this);
}.bind(this))
.then(function(){
loading.resolve(this.contents);
}.bind(this))
.catch(function(error){
loading.reject(error);
});
}
return loaded;
}
/**
* Adds a base tag for resolving urls in the section
* @private
*/
base(){
return replaceBase(this.document, this);
}
/**
* Render the contents of a section
* @param {method} [_request] a request method to use for loading
* @return {string} output a serialized XML Document
*/
render(_request){
var rendering = new defer();
var rendered = rendering.promise;
this.output; // TODO: better way to return this from hooks?
this.load(_request).
then(function(contents){
var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
var isIE = userAgent.indexOf('Trident') >= 0;
var Serializer;
if (typeof XMLSerializer === "undefined" || isIE) {
Serializer = XMLDOMSerializer;
} else {
Serializer = XMLSerializer;
}
var serializer = new Serializer();
this.output = serializer.serializeToString(contents);
return this.output;
}.bind(this)).
then(function(){
return this.hooks.serialize.trigger(this.output, this);
}.bind(this)).
then(function(){
rendering.resolve(this.output);
}.bind(this))
.catch(function(error){
rendering.reject(error);
});
return rendered;
}
/**
* 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;
};
/**
* 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.
* @param {object} globalLayout The global layout settings object, chapter properties string
* @return {object} layoutProperties Object with layout properties
*/
reconcileLayoutSettings(globalLayout){
//-- Get the global defaults
var settings = {
layout : globalLayout.layout,
spread : globalLayout.spread,
orientation : globalLayout.orientation
};
//-- Get the chapter's display type
this.properties.forEach(function(prop){
var rendition = prop.replace("rendition:", "");
var split = rendition.indexOf("-");
var property, value;
if(split != -1){
property = rendition.slice(0, split);
value = rendition.slice(split+1);
settings[property] = value;
}
});
return settings;
}
/**
* Get a CFI from a Range in the Section
* @param {range} _range
* @return {string} cfi an EpubCFI string
*/
cfiFromRange(_range) {
return new EpubCFI(_range, this.cfiBase).toString();
}
/**
* Get a CFI from an Element in the Section
* @param {element} el
* @return {string} cfi an EpubCFI string
*/
cfiFromElement(el) {
return new EpubCFI(el, this.cfiBase).toString();
}
/**
* Unload the section document
*/
unload() {
this.document = undefined;
this.contents = undefined;
this.output = undefined;
}
destroy() {
this.unload();
this.hooks.serialize.clear();
this.hooks.content.clear();
this.hooks = undefined;
this.idref = undefined;
this.linear = undefined;
this.properties = undefined;
this.index = undefined;
this.href = undefined;
this.url = undefined;
this.next = undefined;
this.prev = undefined;
this.cfiBase = undefined;
}
}
export default Section;

View file

@ -0,0 +1,274 @@
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

@ -0,0 +1,385 @@
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 "events";
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 extends EventEmitter {
constructor(name, requester, resolver) {
super();
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();
}
}
Object.assign(Store.prototype, EventEmitter.prototype);
export default Store;

View file

@ -0,0 +1,268 @@
import Url from "./utils/url";
/**
* Themes to apply to displayed content
* @class
* @param {Rendition} rendition
*/
class Themes {
constructor(rendition) {
this.rendition = rendition;
this._themes = {
"default" : {
"rules" : {},
"url" : "",
"serialized" : ""
}
};
this._overrides = {};
this._current = "default";
this._injected = [];
this.rendition.hooks.content.register(this.inject.bind(this));
this.rendition.hooks.content.register(this.overrides.bind(this));
}
/**
* Add themes to be used by a rendition
* @param {object | Array<object> | string}
* @example themes.register("light", "http://example.com/light.css")
* @example themes.register("light", { "body": { "color": "purple"}})
* @example themes.register({ "light" : {...}, "dark" : {...}})
*/
register () {
if (arguments.length === 0) {
return;
}
if (arguments.length === 1 && typeof(arguments[0]) === "object") {
return this.registerThemes(arguments[0]);
}
if (arguments.length === 1 && typeof(arguments[0]) === "string") {
return this.default(arguments[0]);
}
if (arguments.length === 2 && typeof(arguments[1]) === "string") {
return this.registerUrl(arguments[0], arguments[1]);
}
if (arguments.length === 2 && typeof(arguments[1]) === "object") {
return this.registerRules(arguments[0], arguments[1]);
}
}
/**
* Add a default theme to be used by a rendition
* @param {object | string} theme
* @example themes.register("http://example.com/default.css")
* @example themes.register({ "body": { "color": "purple"}})
*/
default (theme) {
if (!theme) {
return;
}
if (typeof(theme) === "string") {
return this.registerUrl("default", theme);
}
if (typeof(theme) === "object") {
return this.registerRules("default", theme);
}
}
/**
* Register themes object
* @param {object} themes
*/
registerThemes (themes) {
for (var theme in themes) {
if (themes.hasOwnProperty(theme)) {
if (typeof(themes[theme]) === "string") {
this.registerUrl(theme, themes[theme]);
} else {
this.registerRules(theme, themes[theme]);
}
}
}
}
/**
* Register a theme by passing its css as string
* @param {string} name
* @param {string} css
*/
registerCss (name, css) {
this._themes[name] = { "serialized" : css };
if (this._injected[name] || name == 'default') {
this.update(name);
}
}
/**
* Register a url
* @param {string} name
* @param {string} input
*/
registerUrl (name, input) {
var url = new Url(input);
this._themes[name] = { "url": url.toString() };
if (this._injected[name] || name == 'default') {
this.update(name);
}
}
/**
* Register rule
* @param {string} name
* @param {object} rules
*/
registerRules (name, rules) {
this._themes[name] = { "rules": rules };
// TODO: serialize css rules
if (this._injected[name] || name == 'default') {
this.update(name);
}
}
/**
* Select a theme
* @param {string} name
*/
select (name) {
var prev = this._current;
var contents;
this._current = name;
this.update(name);
contents = this.rendition.getContents();
contents.forEach( (content) => {
content.removeClass(prev);
content.addClass(name);
});
}
/**
* Update a theme
* @param {string} name
*/
update (name) {
var contents = this.rendition.getContents();
contents.forEach( (content) => {
this.add(name, content);
});
}
/**
* Inject all themes into contents
* @param {Contents} contents
*/
inject (contents) {
var links = [];
var themes = this._themes;
var theme;
for (var name in themes) {
if (themes.hasOwnProperty(name) && (name === this._current || name === "default")) {
theme = themes[name];
if((theme.rules && Object.keys(theme.rules).length > 0) || (theme.url && links.indexOf(theme.url) === -1)) {
this.add(name, contents);
}
this._injected.push(name);
}
}
if(this._current != "default") {
contents.addClass(this._current);
}
}
/**
* Add Theme to contents
* @param {string} name
* @param {Contents} contents
*/
add (name, contents) {
var theme = this._themes[name];
if (!theme || !contents) {
return;
}
if (theme.url) {
contents.addStylesheet(theme.url);
} else if (theme.serialized) {
contents.addStylesheetCss(theme.serialized, name);
theme.injected = true;
} else if (theme.rules) {
contents.addStylesheetRules(theme.rules, name);
theme.injected = true;
}
}
/**
* Add override
* @param {string} name
* @param {string} value
* @param {boolean} priority
*/
override (name, value, priority) {
var contents = this.rendition.getContents();
this._overrides[name] = {
value: value,
priority: priority === true
};
contents.forEach( (content) => {
content.css(name, this._overrides[name].value, this._overrides[name].priority);
});
}
removeOverride (name) {
var contents = this.rendition.getContents();
delete this._overrides[name];
contents.forEach( (content) => {
content.css(name);
});
}
/**
* Add all overrides
* @param {Content} content
*/
overrides (contents) {
var overrides = this._overrides;
for (var rule in overrides) {
if (overrides.hasOwnProperty(rule)) {
contents.css(rule, overrides[rule].value, overrides[rule].priority);
}
}
}
/**
* Adjust the font size of a rendition
* @param {number} size
*/
fontSize (size) {
this.override("font-size", size);
}
/**
* Adjust the font-family of a rendition
* @param {string} f
*/
font (f) {
this.override("font-family", f, true);
}
destroy() {
this.rendition = undefined;
this._themes = undefined;
this._overrides = undefined;
this._current = undefined;
this._injected = undefined;
}
}
export default Themes;

View file

@ -0,0 +1,62 @@
export const EPUBJS_VERSION = "0.3";
// Dom events to listen for
export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "mousemove", "click", "touchend", "touchstart", "touchmove"];
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"
}
}

View file

@ -0,0 +1,876 @@
/**
* Core Utilities and Helpers
* @module Core
*/
import { DOMParser as XMLDOMParser } from "@xmldom/xmldom";
/**
* Vendor prefixed requestAnimationFrame
* @returns {function} requestAnimationFrame
* @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);
/**
* Generates a UUID
* based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
* @returns {string} uuid
* @memberof Core
*/
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);
});
return uuid;
}
/**
* Gets the height of a document
* @returns {number} height
* @memberof Core
*/
export function documentHeight() {
return Math.max(
document.documentElement.clientHeight,
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight
);
}
/**
* Checks if a node is an element
* @param {object} obj
* @returns {boolean}
* @memberof Core
*/
export function isElement(obj) {
return !!(obj && obj.nodeType == 1);
}
/**
* @param {any} n
* @returns {boolean}
* @memberof Core
*/
export function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
/**
* @param {any} n
* @returns {boolean}
* @memberof Core
*/
export function isFloat(n) {
let f = parseFloat(n);
if (isNumber(n) === false) {
return false;
}
if (typeof n === "string" && n.indexOf(".") > -1) {
return true;
}
return Math.floor(f) !== f;
}
/**
* Get a prefixed css property
* @param {string} unprefixed
* @returns {string}
* @memberof Core
*/
export function prefixed(unprefixed) {
var vendors = ["Webkit", "webkit", "Moz", "O", "ms" ];
var prefixes = ["-webkit-", "-webkit-", "-moz-", "-o-", "-ms-"];
var lower = unprefixed.toLowerCase();
var length = vendors.length;
if (typeof(document) === "undefined" || typeof(document.body.style[lower]) != "undefined") {
return unprefixed;
}
for (var i = 0; i < length; i++) {
if (typeof(document.body.style[prefixes[i] + lower]) != "undefined") {
return prefixes[i] + lower;
}
}
return unprefixed;
}
/**
* Apply defaults to an object
* @param {object} obj
* @returns {object}
* @memberof Core
*/
export function defaults(obj) {
for (var i = 1, length = arguments.length; i < length; i++) {
var source = arguments[i];
for (var prop in source) {
if (obj[prop] === void 0) obj[prop] = source[prop];
}
}
return obj;
}
/**
* Extend properties of an object
* @param {object} target
* @returns {object}
* @memberof Core
*/
export function extend(target) {
var sources = [].slice.call(arguments, 1);
sources.forEach(function (source) {
if(!source) return;
Object.getOwnPropertyNames(source).forEach(function(propName) {
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
});
return target;
}
/**
* Fast quicksort insert for sorted array -- based on:
* http://stackoverflow.com/questions/1344500/efficient-way-to-insert-a-number-into-a-sorted-array-of-numbers
* @param {any} item
* @param {array} array
* @param {function} [compareFunction]
* @returns {number} location (in array)
* @memberof Core
*/
export function insert(item, array, compareFunction) {
var location = locationOf(item, array, compareFunction);
array.splice(location, 0, item);
return location;
}
/**
* Finds where something would fit into a sorted array
* @param {any} item
* @param {array} array
* @param {function} [compareFunction]
* @param {function} [_start]
* @param {function} [_end]
* @returns {number} location (in array)
* @memberof Core
*/
export function locationOf(item, array, compareFunction, _start, _end) {
var start = _start || 0;
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(end-start <= 0) {
return pivot;
}
compared = compareFunction(array[pivot], item);
if(end-start === 1) {
return compared >= 0 ? pivot : pivot + 1;
}
if(compared === 0) {
return pivot;
}
if(compared === -1) {
return locationOf(item, array, compareFunction, pivot, end);
} else{
return locationOf(item, array, compareFunction, start, pivot);
}
}
/**
* Finds index of something in a sorted array
* Returns -1 if not found
* @param {any} item
* @param {array} array
* @param {function} [compareFunction]
* @param {function} [_start]
* @param {function} [_end]
* @returns {number} index (in array) or -1
* @memberof Core
*/
export function indexOfSorted(item, array, compareFunction, _start, _end) {
var start = _start || 0;
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(end-start <= 0) {
return -1; // Not found
}
compared = compareFunction(array[pivot], item);
if(end-start === 1) {
return compared === 0 ? pivot : -1;
}
if(compared === 0) {
return pivot; // Found
}
if(compared === -1) {
return indexOfSorted(item, array, compareFunction, pivot, end);
} else{
return indexOfSorted(item, array, compareFunction, start, pivot);
}
}
/**
* Find the bounds of an element
* taking padding and margin into account
* @param {element} el
* @returns {{ width: Number, height: Number}}
* @memberof Core
*/
export function bounds(el) {
var style = window.getComputedStyle(el);
var widthProps = ["width", "paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
var heightProps = ["height", "paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
var width = 0;
var height = 0;
widthProps.forEach(function(prop){
width += parseFloat(style[prop]) || 0;
});
heightProps.forEach(function(prop){
height += parseFloat(style[prop]) || 0;
});
return {
height: height,
width: width
};
}
/**
* Find the bounds of an element
* taking padding, margin and borders into account
* @param {element} el
* @returns {{ width: Number, height: Number}}
* @memberof Core
*/
export function borders(el) {
var style = window.getComputedStyle(el);
var widthProps = ["paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
var heightProps = ["paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
var width = 0;
var height = 0;
widthProps.forEach(function(prop){
width += parseFloat(style[prop]) || 0;
});
heightProps.forEach(function(prop){
height += parseFloat(style[prop]) || 0;
});
return {
height: height,
width: width
};
}
/**
* Find the bounds of any node
* allows for getting bounds of text nodes by wrapping them in a range
* @param {node} node
* @returns {BoundingClientRect}
* @memberof Core
*/
export function nodeBounds(node) {
let elPos;
let doc = node.ownerDocument;
if(node.nodeType == Node.TEXT_NODE){
let elRange = doc.createRange();
elRange.selectNodeContents(node);
elPos = elRange.getBoundingClientRect();
} else {
elPos = node.getBoundingClientRect();
}
return elPos;
}
/**
* Find the equivalent of getBoundingClientRect of a browser window
* @returns {{ width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }}
* @memberof Core
*/
export function windowBounds() {
var width = window.innerWidth;
var height = window.innerHeight;
return {
top: 0,
left: 0,
right: width,
bottom: height,
width: width,
height: height
};
}
/**
* Gets the index of a node in its parent
* @param {Node} node
* @param {string} typeId
* @return {number} index
* @memberof Core
*/
export function indexOfNode(node, typeId) {
var parent = node.parentNode;
var children = parent.childNodes;
var sib;
var index = -1;
for (var i = 0; i < children.length; i++) {
sib = children[i];
if (sib.nodeType === typeId) {
index++;
}
if (sib == node) break;
}
return index;
}
/**
* Gets the index of a text node in its parent
* @param {node} textNode
* @returns {number} index
* @memberof Core
*/
export function indexOfTextNode(textNode) {
return indexOfNode(textNode, TEXT_NODE);
}
/**
* Gets the index of an element node in its parent
* @param {element} elementNode
* @returns {number} index
* @memberof Core
*/
export function indexOfElementNode(elementNode) {
return indexOfNode(elementNode, ELEMENT_NODE);
}
/**
* Check if extension is xml
* @param {string} ext
* @returns {boolean}
* @memberof Core
*/
export function isXml(ext) {
return ["xml", "opf", "ncx"].indexOf(ext) > -1;
}
/**
* Create a new blob
* @param {any} content
* @param {string} mime
* @returns {Blob}
* @memberof Core
*/
export function createBlob(content, mime){
return new Blob([content], {type : mime });
}
/**
* Create a new blob url
* @param {any} content
* @param {string} mime
* @returns {string} url
* @memberof Core
*/
export function createBlobUrl(content, mime){
var tempUrl;
var blob = createBlob(content, mime);
tempUrl = _URL.createObjectURL(blob);
return tempUrl;
}
/**
* Remove a blob url
* @param {string} url
* @memberof Core
*/
export function revokeBlobUrl(url){
return _URL.revokeObjectURL(url);
}
/**
* Create a new base64 encoded url
* @param {any} content
* @param {string} mime
* @returns {string} url
* @memberof Core
*/
export function createBase64Url(content, mime){
var data;
var datauri;
if (typeof(content) !== "string") {
// Only handles strings
return;
}
data = btoa(content);
datauri = "data:" + mime + ";base64," + data;
return datauri;
}
/**
* Get type of an object
* @param {object} obj
* @returns {string} type
* @memberof Core
*/
export function type(obj){
return Object.prototype.toString.call(obj).slice(8, -1);
}
/**
* Parse xml (or html) markup
* @param {string} markup
* @param {string} mime
* @param {boolean} forceXMLDom force using 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;
}
// Remove byte order mark before parsing
// https://www.w3.org/International/questions/qa-byte-order-mark
if(markup.charCodeAt(0) === 0xFEFF) {
markup = markup.slice(1);
}
doc = new Parser().parseFromString(markup, mime);
return doc;
}
/**
* querySelector polyfill
* @param {element} el
* @param {string} sel selector string
* @returns {element} element
* @memberof Core
*/
export function qs(el, sel) {
var elements;
if (!el) {
throw new Error("No Element Provided");
}
if (typeof el.querySelector != "undefined") {
return el.querySelector(sel);
} else {
elements = el.getElementsByTagName(sel);
if (elements.length) {
return elements[0];
}
}
}
/**
* querySelectorAll polyfill
* @param {element} el
* @param {string} sel selector string
* @returns {element[]} elements
* @memberof Core
*/
export function qsa(el, sel) {
if (typeof el.querySelector != "undefined") {
return el.querySelectorAll(sel);
} else {
return el.getElementsByTagName(sel);
}
}
/**
* querySelector by property
* @param {element} el
* @param {string} sel selector string
* @param {object[]} props
* @returns {element[]} elements
* @memberof Core
*/
export function qsp(el, sel, props) {
var q, filtered;
if (typeof el.querySelector != "undefined") {
sel += "[";
for (var prop in 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) {
for (var prop in props) {
if(el.getAttribute(prop) === props[prop]){
return true;
}
}
return false;
});
if (filtered) {
return filtered[0];
}
}
}
/**
* Sprint through all text nodes in a document
* @memberof Core
* @param {element} root element to start with
* @param {function} func function to run on each element
*/
export function sprint(root, func) {
var doc = root.ownerDocument || root;
if (typeof(doc.createTreeWalker) !== "undefined") {
treeWalker(root, func, NodeFilter.SHOW_TEXT);
} else {
walk(root, function(node) {
if (node && node.nodeType === 3) { // Node.TEXT_NODE
func(node);
}
}, true);
}
}
/**
* Create a treeWalker
* @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
*/
export function treeWalker(root, func, filter) {
var treeWalker = document.createTreeWalker(root, filter, null, false);
let node;
while ((node = treeWalker.nextNode())) {
func(node);
}
}
/**
* @memberof Core
* @param {node} node
* @param {callback} return false for continue,true for break inside callback
*/
export function walk(node,callback){
if(callback(node)){
return true;
}
node = node.firstChild;
if(node){
do{
let walked = walk(node,callback);
if(walked){
return true;
}
node = node.nextSibling;
} while(node);
}
}
/**
* Convert a blob to a base64 encoded string
* @param {Blog} blob
* @returns {string}
* @memberof Core
*/
export function blob2base64(blob) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
resolve(reader.result);
};
});
}
/**
* Creates a new pending promise and provides methods to resolve or reject it.
* From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible
* @memberof Core
*/
export function defer() {
/* A method to resolve the associated Promise with the value passed.
* If the promise is already settled it does nothing.
*
* @param {anything} value : This value is used to resolve the promise
* If the value is a Promise then the associated promise assumes the state
* of Promise passed as value.
*/
this.resolve = null;
/* A method to reject the associated 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.
* Generally its an Error object. If however a Promise is passed, then the Promise
* itself will be the reason for rejection no matter the state of the Promise.
*/
this.reject = null;
this.id = uuid();
/* A newly created Pomise object.
* Initially in pending state.
*/
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
Object.freeze(this);
}
/**
* querySelector with filter by epub type
* @param {element} html
* @param {string} element element type to find
* @param {string} type epub type to find
* @returns {element[]} elements
* @memberof Core
*/
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) {
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) {
return query[i];
}
}
} else {
return query;
}
}
/**
* Find direct descendents of an element
* @param {element} el
* @returns {element[]} children
* @memberof Core
*/
export function findChildren(el) {
var result = [];
var childNodes = el.childNodes;
for (var i = 0; i < childNodes.length; i++) {
let node = childNodes[i];
if (node.nodeType === 1) {
result.push(node);
}
}
return result;
}
/**
* Find all parents (ancestors) of an element
* @param {element} node
* @returns {element[]} parents
* @memberof Core
*/
export function parents(node) {
var nodes = [node];
for (; node; node = node.parentNode) {
nodes.unshift(node);
}
return nodes
}
/**
* Find all direct descendents of a specific type
* @param {element} el
* @param {string} nodeName
* @param {boolean} [single]
* @returns {element[]} children
* @memberof Core
*/
export function filterChildren(el, nodeName, single) {
var result = [];
var childNodes = el.childNodes;
for (var i = 0; i < childNodes.length; i++) {
let node = childNodes[i];
if (node.nodeType === 1 && node.nodeName.toLowerCase() === nodeName) {
if (single) {
return node;
} else {
result.push(node);
}
}
}
if (!single) {
return result;
}
}
/**
* Filter all parents (ancestors) with tag name
* @param {element} node
* @param {string} tagname
* @returns {element[]} parents
* @memberof Core
*/
export function getParentByTagName(node, tagname) {
let parent;
if (node === null || tagname === '') return;
parent = node.parentNode;
while (parent.nodeType === 1) {
if (parent.tagName.toLowerCase() === tagname) {
return parent;
}
parent = parent.parentNode;
}
}
/**
* Lightweight Polyfill for DOM Range
* @class
* @memberof Core
*/
export class RangeObject {
constructor() {
this.collapsed = false;
this.commonAncestorContainer = undefined;
this.endContainer = undefined;
this.endOffset = undefined;
this.startContainer = undefined;
this.startOffset = undefined;
}
setStart(startNode, startOffset) {
this.startContainer = startNode;
this.startOffset = startOffset;
if (!this.endContainer) {
this.collapse(true);
} else {
this.commonAncestorContainer = this._commonAncestorContainer();
}
this._checkCollapsed();
}
setEnd(endNode, endOffset) {
this.endContainer = endNode;
this.endOffset = endOffset;
if (!this.startContainer) {
this.collapse(false);
} else {
this.collapsed = false;
this.commonAncestorContainer = this._commonAncestorContainer();
}
this._checkCollapsed();
}
collapse(toStart) {
this.collapsed = true;
if (toStart) {
this.endContainer = this.startContainer;
this.endOffset = this.startOffset;
this.commonAncestorContainer = this.startContainer.parentNode;
} else {
this.startContainer = this.endContainer;
this.startOffset = this.endOffset;
this.commonAncestorContainer = this.endOffset.parentNode;
}
}
selectNode(referenceNode) {
let parent = referenceNode.parentNode;
let index = Array.prototype.indexOf.call(parent.childNodes, referenceNode);
this.setStart(parent, index);
this.setEnd(parent, index + 1);
}
selectNodeContents(referenceNode) {
let end = referenceNode.childNodes[referenceNode.childNodes - 1];
let endIndex = (referenceNode.nodeType === 3) ?
referenceNode.textContent.length : parent.childNodes.length;
this.setStart(referenceNode, 0);
this.setEnd(referenceNode, endIndex);
}
_commonAncestorContainer(startContainer, endContainer) {
var startParents = parents(startContainer || this.startContainer);
var endParents = parents(endContainer || this.endContainer);
if (startParents[0] != endParents[0]) return undefined;
for (var i = 0; i < startParents.length; i++) {
if (startParents[i] != endParents[i]) {
return startParents[i - 1];
}
}
}
_checkCollapsed() {
if (this.startContainer === this.endContainer &&
this.startOffset === this.endOffset) {
this.collapsed = true;
} else {
this.collapsed = false;
}
}
toString() {
// TODO: implement walking between start and end to find text
}
}

View file

@ -0,0 +1,82 @@
/**
* Hooks allow for injecting functions that must all complete in order before finishing
* They will execute in parallel but all must finish before continuing
* Functions may return a promise if they are async.
* @param {any} context scope of this
* @example this.content = new EPUBJS.Hook(this);
*/
class Hook {
constructor(context){
this.context = context || this;
this.hooks = [];
}
/**
* Adds a function to be run before a hook completes
* @example this.content.register(function(){...});
*/
register(){
for(var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] === "function") {
this.hooks.push(arguments[i]);
} else {
// unpack array
for(var j = 0; j < arguments[i].length; ++j) {
this.hooks.push(arguments[i][j]);
}
}
}
}
/**
* 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(){...});
*/
trigger(){
var args = arguments;
var context = this.context;
var promises = [];
this.hooks.forEach(function(task) {
try {
var executing = task.apply(context, args);
} catch (err) {
console.log(err);
}
if(executing && typeof executing["then"] === "function") {
// Task is a function that returns a promise
promises.push(executing);
}
// Otherwise Task resolves immediately, continue
});
return Promise.all(promises);
}
// Adds a function to be run before a hook completes
list(){
return this.hooks;
}
clear(){
return this.hooks = [];
}
}
export default Hook;

View file

@ -0,0 +1,169 @@
/*
From Zip.js, by Gildas Lormeau
edited down
*/
var table = {
"application" : {
"ecmascript" : [ "es", "ecma" ],
"javascript" : "js",
"ogg" : "ogx",
"pdf" : "pdf",
"postscript" : [ "ps", "ai", "eps", "epsi", "epsf", "eps2", "eps3" ],
"rdf+xml" : "rdf",
"smil" : [ "smi", "smil" ],
"xhtml+xml" : [ "xhtml", "xht" ],
"xml" : [ "xml", "xsl", "xsd", "opf", "ncx" ],
"zip" : "zip",
"x-httpd-eruby" : "rhtml",
"x-latex" : "latex",
"x-maker" : [ "frm", "maker", "frame", "fm", "fb", "book", "fbdoc" ],
"x-object" : "o",
"x-shockwave-flash" : [ "swf", "swfl" ],
"x-silverlight" : "scr",
"epub+zip" : "epub",
"font-tdpfr" : "pfr",
"inkml+xml" : [ "ink", "inkml" ],
"json" : "json",
"jsonml+json" : "jsonml",
"mathml+xml" : "mathml",
"metalink+xml" : "metalink",
"mp4" : "mp4s",
// "oebps-package+xml" : "opf",
"omdoc+xml" : "omdoc",
"oxps" : "oxps",
"vnd.amazon.ebook" : "azw",
"widget" : "wgt",
// "x-dtbncx+xml" : "ncx",
"x-dtbook+xml" : "dtb",
"x-dtbresource+xml" : "res",
"x-font-bdf" : "bdf",
"x-font-ghostscript" : "gsf",
"x-font-linux-psf" : "psf",
"x-font-otf" : "otf",
"x-font-pcf" : "pcf",
"x-font-snf" : "snf",
"x-font-ttf" : [ "ttf", "ttc" ],
"x-font-type1" : [ "pfa", "pfb", "pfm", "afm" ],
"x-font-woff" : "woff",
"x-mobipocket-ebook" : [ "prc", "mobi" ],
"x-mspublisher" : "pub",
"x-nzb" : "nzb",
"x-tgif" : "obj",
"xaml+xml" : "xaml",
"xml-dtd" : "dtd",
"xproc+xml" : "xpl",
"xslt+xml" : "xslt",
"internet-property-stream" : "acx",
"x-compress" : "z",
"x-compressed" : "tgz",
"x-gzip" : "gz",
},
"audio" : {
"flac" : "flac",
"midi" : [ "mid", "midi", "kar", "rmi" ],
"mpeg" : [ "mpga", "mpega", "mp2", "mp3", "m4a", "mp2a", "m2a", "m3a" ],
"mpegurl" : "m3u",
"ogg" : [ "oga", "ogg", "spx" ],
"x-aiff" : [ "aif", "aiff", "aifc" ],
"x-ms-wma" : "wma",
"x-wav" : "wav",
"adpcm" : "adp",
"mp4" : "mp4a",
"webm" : "weba",
"x-aac" : "aac",
"x-caf" : "caf",
"x-matroska" : "mka",
"x-pn-realaudio-plugin" : "rmp",
"xm" : "xm",
"mid" : [ "mid", "rmi" ]
},
"image" : {
"gif" : "gif",
"ief" : "ief",
"jpeg" : [ "jpeg", "jpg", "jpe" ],
"pcx" : "pcx",
"png" : "png",
"svg+xml" : [ "svg", "svgz" ],
"tiff" : [ "tiff", "tif" ],
"x-icon" : "ico",
"bmp" : "bmp",
"webp" : "webp",
"x-pict" : [ "pic", "pct" ],
"x-tga" : "tga",
"cis-cod" : "cod"
},
"text" : {
"cache-manifest" : [ "manifest", "appcache" ],
"css" : "css",
"csv" : "csv",
"html" : [ "html", "htm", "shtml", "stm" ],
"mathml" : "mml",
"plain" : [ "txt", "text", "brf", "conf", "def", "list", "log", "in", "bas" ],
"richtext" : "rtx",
"tab-separated-values" : "tsv",
"x-bibtex" : "bib"
},
"video" : {
"mpeg" : [ "mpeg", "mpg", "mpe", "m1v", "m2v", "mp2", "mpa", "mpv2" ],
"mp4" : [ "mp4", "mp4v", "mpg4" ],
"quicktime" : [ "qt", "mov" ],
"ogg" : "ogv",
"vnd.mpegurl" : [ "mxu", "m4u" ],
"x-flv" : "flv",
"x-la-asf" : [ "lsf", "lsx" ],
"x-mng" : "mng",
"x-ms-asf" : [ "asf", "asx", "asr" ],
"x-ms-wm" : "wm",
"x-ms-wmv" : "wmv",
"x-ms-wmx" : "wmx",
"x-ms-wvx" : "wvx",
"x-msvideo" : "avi",
"x-sgi-movie" : "movie",
"x-matroska" : [ "mpv", "mkv", "mk3d", "mks" ],
"3gpp2" : "3g2",
"h261" : "h261",
"h263" : "h263",
"h264" : "h264",
"jpeg" : "jpgv",
"jpm" : [ "jpm", "jpgm" ],
"mj2" : [ "mj2", "mjp2" ],
"vnd.ms-playready.media.pyv" : "pyv",
"vnd.uvvu.mp4" : [ "uvu", "uvvu" ],
"vnd.vivo" : "viv",
"webm" : "webm",
"x-f4v" : "f4v",
"x-m4v" : "m4v",
"x-ms-vob" : "vob",
"x-smv" : "smv"
}
};
var mimeTypes = (function() {
var type, subtype, val, index, mimeTypes = {};
for (type in table) {
if (table.hasOwnProperty(type)) {
for (subtype in table[type]) {
if (table[type].hasOwnProperty(subtype)) {
val = table[type][subtype];
if (typeof val == "string") {
mimeTypes[val] = type + "/" + subtype;
} else {
for (index = 0; index < val.length; index++) {
mimeTypes[val[index]] = type + "/" + subtype;
}
}
}
}
}
}
return mimeTypes;
})();
var defaultValue = "text/plain";//"application/octet-stream";
function lookup(filename) {
return filename && mimeTypes[filename.split(".").pop().toLowerCase()] || defaultValue;
};
export default { lookup };

View file

@ -0,0 +1,102 @@
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

@ -0,0 +1,246 @@
import {defer, requestAnimationFrame} from "./core";
/**
* Queue for handling tasks one at a time
* @class
* @param {scope} context what this will resolve to in the tasks
*/
class Queue {
constructor(context){
this._q = [];
this.context = context;
this.tick = requestAnimationFrame;
this.running = false;
this.paused = false;
}
/**
* Add an item to the queue
* @return {Promise}
*/
enqueue() {
var deferred, promise;
var queued;
var task = [].shift.call(arguments);
var args = arguments;
// Handle single args without context
// if(args && !Array.isArray(args)) {
// args = [args];
// }
if(!task) {
throw new Error("No Task Provided");
}
if(typeof task === "function"){
deferred = new defer();
promise = deferred.promise;
queued = {
"task" : task,
"args" : args,
//"context" : context,
"deferred" : deferred,
"promise" : promise
};
} else {
// Task is a promise
queued = {
"promise" : task
};
}
this._q.push(queued);
// Wait to start queue flush
if (this.paused == false && !this.running) {
// setTimeout(this.flush.bind(this), 0);
// this.tick.call(window, this.run.bind(this));
this.run();
}
return queued.promise;
}
/**
* Run one item
* @return {Promise}
*/
dequeue(){
var inwait, task, result;
if(this._q.length && !this.paused) {
inwait = this._q.shift();
task = inwait.task;
if(task){
// console.log(task)
result = task.apply(this.context, inwait.args);
if(result && typeof result["then"] === "function") {
// Task is a function that returns a promise
return result.then(function(){
inwait.deferred.resolve.apply(this.context, arguments);
}.bind(this), function() {
inwait.deferred.reject.apply(this.context, arguments);
}.bind(this));
} else {
// Task resolves immediately
inwait.deferred.resolve.apply(this.context, result);
return inwait.promise;
}
} else if(inwait.promise) {
// Task is a promise
return inwait.promise;
}
} else {
inwait = new defer();
inwait.deferred.resolve();
return inwait.promise;
}
}
// Run All Immediately
dump(){
while(this._q.length) {
this.dequeue();
}
}
/**
* Run all tasks sequentially, at convince
* @return {Promise}
*/
run(){
if(!this.running){
this.running = true;
this.defered = new defer();
}
this.tick.call(window, () => {
if(this._q.length) {
this.dequeue()
.then(function(){
this.run();
}.bind(this));
} else {
this.defered.resolve();
this.running = undefined;
}
});
// Unpause
if(this.paused == true) {
this.paused = false;
}
return this.defered.promise;
}
/**
* Flush all, as quickly as possible
* @return {Promise}
*/
flush(){
if(this.running){
return this.running;
}
if(this._q.length) {
this.running = this.dequeue()
.then(function(){
this.running = undefined;
return this.flush();
}.bind(this));
return this.running;
}
}
/**
* Clear all items in wait
*/
clear(){
this._q = [];
}
/**
* Get the number of tasks in the queue
* @return {number} tasks
*/
length(){
return this._q.length;
}
/**
* Pause a running queue
*/
pause(){
this.paused = true;
}
/**
* End the queue
*/
stop(){
this._q = [];
this.running = false;
this.paused = true;
}
}
/**
* Create a new task from a callback
* @class
* @private
* @param {function} task
* @param {array} args
* @param {scope} context
* @return {function} task
*/
class Task {
constructor(task, args, context){
return function(){
var toApply = arguments || [];
return new Promise( (resolve, reject) => {
var callback = function(value, err){
if (!value && err) {
reject(err);
} else {
resolve(value);
}
};
// Add the callback to the arguments list
toApply.push(callback);
// Apply all arguments to the functions
task.apply(context || this, toApply);
});
};
}
}
export default Queue;
export { Task };

View file

@ -0,0 +1,138 @@
import { qs, qsa } from "./core";
import Url from "./url";
import Path from "./path";
export function replaceBase(doc, section){
var base;
var head;
var url = section.url;
var absolute = (url.indexOf("://") > -1);
if(!doc){
return;
}
head = qs(doc, "head");
base = qs(head, "base");
if(!base) {
base = doc.createElement("base");
head.insertBefore(base, head.firstChild);
}
// Fix for Safari crashing if the url doesn't have an origin
if (!absolute && window && window.location) {
url = window.location.origin + url;
}
base.setAttribute("href", url);
}
export function replaceCanonical(doc, section){
var head;
var link;
var url = section.canonical;
if(!doc){
return;
}
head = qs(doc, "head");
link = qs(head, "link[rel='canonical']");
if (link) {
link.setAttribute("href", url);
} else {
link = doc.createElement("link");
link.setAttribute("rel", "canonical");
link.setAttribute("href", url);
head.appendChild(link);
}
}
export function replaceMeta(doc, section){
var head;
var meta;
var id = section.idref;
if(!doc){
return;
}
head = qs(doc, "head");
meta = qs(head, "link[property='dc.identifier']");
if (meta) {
meta.setAttribute("content", id);
} else {
meta = doc.createElement("meta");
meta.setAttribute("name", "dc.identifier");
meta.setAttribute("content", id);
head.appendChild(meta);
}
}
// TODO: move me to Contents
export function replaceLinks(contents, fn) {
var 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){
var href = link.getAttribute("href");
if(href.indexOf("mailto:") === 0){
return;
}
var absolute = (href.indexOf("://") > -1);
if(absolute){
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);
}
return false;
};
}
}.bind(this);
for (var i = 0; i < links.length; i++) {
replaceLink(links[i]);
}
}
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]);
}
});
return content;
}

View file

@ -0,0 +1,150 @@
import {defer, isXml, parse} from "./core";
import Path from "./path";
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");
}
// If type isn"t set, determine it from the file extension
if(!type) {
type = new Path(url).extension;
}
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 {
deferred.reject({
status: this.status,
message : this.response,
stack : new Error().stack
});
}
}
}
return deferred.promise;
}
export default request;

View file

@ -0,0 +1,55 @@
// Detect RTL scroll type
// Based on https://github.com/othree/jquery.rtl-scroll-type/blob/master/src/jquery.rtl-scroll.js
export default function scrollType() {
var type = "reverse";
var definer = createDefiner();
document.body.appendChild(definer);
if (definer.scrollLeft > 0) {
type = "default";
} else {
if (typeof Element !== 'undefined' && Element.prototype.scrollIntoView) {
definer.children[0].children[1].scrollIntoView();
if (definer.scrollLeft < 0) {
type = "negative";
}
} else {
definer.scrollLeft = 1;
if (definer.scrollLeft === 0) {
type = "negative";
}
}
}
document.body.removeChild(definer);
return type;
}
export function createDefiner() {
var definer = document.createElement('div');
definer.dir="rtl";
definer.style.position = "fixed";
definer.style.width = "1px";
definer.style.height = "1px";
definer.style.top = "0px";
definer.style.left = "0px";
definer.style.overflow = "hidden";
var innerDiv = document.createElement('div');
innerDiv.style.width = "2px";
var spanA = document.createElement('span');
spanA.style.width = "1px";
spanA.style.display = "inline-block";
var spanB = document.createElement('span');
spanB.style.width = "1px";
spanB.style.display = "inline-block";
innerDiv.appendChild(spanA);
innerDiv.appendChild(spanB);
definer.appendChild(innerDiv);
return definer;
}

View file

@ -0,0 +1,108 @@
import Path from "./path";
import path from "path-webpack";
/**
* 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;
}
// 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;
}
}
export default Url;