mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Add an EventTarget for transforming the book
And use it to do paginator-specific CSS replacements. And use it to catch loading errors in reader.js. Fixes #14 Closes #18
This commit is contained in:
@@ -112,6 +112,8 @@ The following methods are consumed by `progress.js`, for getting the correct TOC
|
|||||||
- `.splitTOCHref(href)`: given an href string (from the TOC), returns an array, the first element of which is the `id` of the section (see above), and the second element is the fragment identifier (can be any type; see below). May be async.
|
- `.splitTOCHref(href)`: given an href string (from the TOC), returns an array, the first element of which is the `id` of the section (see above), and the second element is the fragment identifier (can be any type; see below). May be async.
|
||||||
- `.getTOCFragment(doc, id)`: given a `Document` object and a fragment identifier (the one provided by `.splitTOCHref()`; see above), returns a `Node` representing the target linked by the TOC item
|
- `.getTOCFragment(doc, id)`: given a `Document` object and a fragment identifier (the one provided by `.splitTOCHref()`; see above), returns a `Node` representing the target linked by the TOC item
|
||||||
|
|
||||||
|
In addition, the `.transformTarget`, if present, can be used to transform the contents of the book as it loads. It is an `EventTarget` with a custom event `"data"`, whose `.detail` is `{ data, type, name }`, where `.data` is either a string or `Blob`, or a `Promise` thereof, `.type` the content type string, and `.name` the identifier of the resource. Event handlers should mutate `.data` to transform the data.
|
||||||
|
|
||||||
Almost all of the properties and methods are optional. At minimum it needs `.sections` and the `.load()` method for the sections, as otherwise there won't be anything to render.
|
Almost all of the properties and methods are optional. At minimum it needs `.sections` and the `.load()` method for the sections, as otherwise there won't be anything to render.
|
||||||
|
|
||||||
### Archived Files
|
### Archived Files
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ class Loader {
|
|||||||
#children = new Map()
|
#children = new Map()
|
||||||
#refCount = new Map()
|
#refCount = new Map()
|
||||||
allowScript = false
|
allowScript = false
|
||||||
|
eventTarget = new EventTarget()
|
||||||
constructor({ loadText, loadBlob, resources }) {
|
constructor({ loadText, loadBlob, resources }) {
|
||||||
this.loadText = loadText
|
this.loadText = loadText
|
||||||
this.loadBlob = loadBlob
|
this.loadBlob = loadBlob
|
||||||
@@ -714,9 +715,15 @@ class Loader {
|
|||||||
// needed only when replacing in (X)HTML w/o parsing (see below)
|
// needed only when replacing in (X)HTML w/o parsing (see below)
|
||||||
//.filter(({ mediaType }) => ![MIME.XHTML, MIME.HTML].includes(mediaType))
|
//.filter(({ mediaType }) => ![MIME.XHTML, MIME.HTML].includes(mediaType))
|
||||||
}
|
}
|
||||||
createURL(href, data, type, parent) {
|
async createURL(href, data, type, parent) {
|
||||||
if (!data) return ''
|
if (!data) return ''
|
||||||
const url = URL.createObjectURL(new Blob([data], { type }))
|
const detail = { data, type }
|
||||||
|
Object.defineProperty(detail, 'name', { value: href }) // readonly
|
||||||
|
const event = new CustomEvent('data', { detail })
|
||||||
|
this.eventTarget.dispatchEvent(event)
|
||||||
|
const newData = await event.detail.data
|
||||||
|
const newType = await event.detail.type
|
||||||
|
const url = URL.createObjectURL(new Blob([newData], { type: newType }))
|
||||||
this.#cache.set(href, url)
|
this.#cache.set(href, url)
|
||||||
this.#refCount.set(href, 1)
|
this.#refCount.set(href, 1)
|
||||||
if (parent) {
|
if (parent) {
|
||||||
@@ -767,7 +774,9 @@ class Loader {
|
|||||||
// prevent circular references
|
// prevent circular references
|
||||||
&& parents.every(p => p !== href)
|
&& parents.every(p => p !== href)
|
||||||
if (shouldReplace) return this.loadReplaced(item, parents)
|
if (shouldReplace) return this.loadReplaced(item, parents)
|
||||||
return this.createURL(href, await this.loadBlob(href), mediaType, parent)
|
// NOTE: this can be replaced with `Promise.try()`
|
||||||
|
const tryLoadBlob = Promise.resolve().then(() => this.loadBlob(href))
|
||||||
|
return this.createURL(href, tryLoadBlob, mediaType, parent)
|
||||||
}
|
}
|
||||||
async loadHref(href, base, parents = []) {
|
async loadHref(href, base, parents = []) {
|
||||||
if (isExternal(href)) return href
|
if (isExternal(href)) return href
|
||||||
@@ -779,7 +788,12 @@ class Loader {
|
|||||||
async loadReplaced(item, parents = []) {
|
async loadReplaced(item, parents = []) {
|
||||||
const { href, mediaType } = item
|
const { href, mediaType } = item
|
||||||
const parent = parents.at(-1)
|
const parent = parents.at(-1)
|
||||||
const str = await this.loadText(href)
|
let str = ''
|
||||||
|
try {
|
||||||
|
str = await this.loadText(href)
|
||||||
|
} catch (e) {
|
||||||
|
return this.createURL(href, Promise.reject(e), mediaType, parent)
|
||||||
|
}
|
||||||
if (!str) return null
|
if (!str) return null
|
||||||
|
|
||||||
// note that one can also just use `replaceString` for everything:
|
// note that one can also just use `replaceString` for everything:
|
||||||
@@ -851,23 +865,10 @@ class Loader {
|
|||||||
(_, url) => this.loadHref(url, href, parents)
|
(_, url) => this.loadHref(url, href, parents)
|
||||||
.then(url => `url("${url}")`))
|
.then(url => `url("${url}")`))
|
||||||
// apart from `url()`, strings can be used for `@import` (but why?!)
|
// apart from `url()`, strings can be used for `@import` (but why?!)
|
||||||
const replacedImports = await replaceSeries(replacedUrls,
|
return replaceSeries(replacedUrls,
|
||||||
/@import\s*["']([^"'\n]*?)["']/gi,
|
/@import\s*["']([^"'\n]*?)["']/gi,
|
||||||
(_, url) => this.loadHref(url, href, parents)
|
(_, url) => this.loadHref(url, href, parents)
|
||||||
.then(url => `@import "${url}"`))
|
.then(url => `@import "${url}"`))
|
||||||
const w = window?.innerWidth ?? 800
|
|
||||||
const h = window?.innerHeight ?? 600
|
|
||||||
return replacedImports
|
|
||||||
// unprefix as most of the props are (only) supported unprefixed
|
|
||||||
.replace(/(?<=[{\s;])-epub-/gi, '')
|
|
||||||
// replace vw and vh as they cause problems with layout
|
|
||||||
.replace(/(\d*\.?\d+)vw/gi, (_, d) => parseFloat(d) * w / 100 + 'px')
|
|
||||||
.replace(/(\d*\.?\d+)vh/gi, (_, d) => parseFloat(d) * h / 100 + 'px')
|
|
||||||
// `page-break-*` unsupported in columns; replace with `column-break-*`
|
|
||||||
.replace(/page-break-(after|before|inside)\s*:/gi, (_, x) =>
|
|
||||||
`-webkit-column-break-${x}:`)
|
|
||||||
.replace(/break-(after|before|inside)\s*:\s*(avoid-)?page/gi, (_, x, y) =>
|
|
||||||
`break-${x}: ${y ?? ''}column`)
|
|
||||||
}
|
}
|
||||||
// find & replace all possible relative paths for all assets without parsing
|
// find & replace all possible relative paths for all assets without parsing
|
||||||
replaceString(str, href, parents = []) {
|
replaceString(str, href, parents = []) {
|
||||||
@@ -965,6 +966,7 @@ ${doc.querySelector('parsererror').innerText}`)
|
|||||||
.then(this.#encryption.getDecoder(uri)),
|
.then(this.#encryption.getDecoder(uri)),
|
||||||
resources: this.resources,
|
resources: this.resources,
|
||||||
})
|
})
|
||||||
|
this.transformTarget = this.#loader.eventTarget
|
||||||
this.sections = this.resources.spine.map((spineItem, index) => {
|
this.sections = this.resources.spine.map((spineItem, index) => {
|
||||||
const { idref, linear, properties = [] } = spineItem
|
const { idref, linear, properties = [] } = spineItem
|
||||||
const item = this.resources.getItemByID(idref)
|
const item = this.resources.getItemByID(idref)
|
||||||
|
|||||||
@@ -636,6 +636,22 @@ export class Paginator extends HTMLElement {
|
|||||||
open(book) {
|
open(book) {
|
||||||
this.bookDir = book.dir
|
this.bookDir = book.dir
|
||||||
this.sections = book.sections
|
this.sections = book.sections
|
||||||
|
book.transformTarget?.addEventListener('data', ({ detail }) => {
|
||||||
|
if (detail.type !== 'text/css') return
|
||||||
|
const w = innerWidth
|
||||||
|
const h = innerHeight
|
||||||
|
detail.data = Promise.resolve(detail.data).then(data => data
|
||||||
|
// unprefix as most of the props are (only) supported unprefixed
|
||||||
|
.replace(/(?<=[{\s;])-epub-/gi, '')
|
||||||
|
// replace vw and vh as they cause problems with layout
|
||||||
|
.replace(/(\d*\.?\d+)vw/gi, (_, d) => parseFloat(d) * w / 100 + 'px')
|
||||||
|
.replace(/(\d*\.?\d+)vh/gi, (_, d) => parseFloat(d) * h / 100 + 'px')
|
||||||
|
// `page-break-*` unsupported in columns; replace with `column-break-*`
|
||||||
|
.replace(/page-break-(after|before|inside)\s*:/gi, (_, x) =>
|
||||||
|
`-webkit-column-break-${x}:`)
|
||||||
|
.replace(/break-(after|before|inside)\s*:\s*(avoid-)?page/gi, (_, x, y) =>
|
||||||
|
`break-${x}: ${y ?? ''}column`))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
#createView() {
|
#createView() {
|
||||||
if (this.#view) {
|
if (this.#view) {
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ class Reader {
|
|||||||
this.view.addEventListener('relocate', this.#onRelocate.bind(this))
|
this.view.addEventListener('relocate', this.#onRelocate.bind(this))
|
||||||
|
|
||||||
const { book } = this.view
|
const { book } = this.view
|
||||||
|
book.transformTarget?.addEventListener('data', ({ detail }) => {
|
||||||
|
detail.data = Promise.resolve(detail.data).catch(e => {
|
||||||
|
console.error(new Error(`Failed to load ${detail.name}`, { cause: e }))
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
})
|
||||||
this.view.renderer.setStyles?.(getCSS(this.style))
|
this.view.renderer.setStyles?.(getCSS(this.style))
|
||||||
this.view.renderer.next()
|
this.view.renderer.next()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user