diff --git a/epub.js b/epub.js index 378ffd8..339942c 100644 --- a/epub.js +++ b/epub.js @@ -671,6 +671,8 @@ class Resources { ?.getAttribute('content')) ?? this.getItemByHref(this.guide ?.find(ref => ref.type.includes('cover'))?.href) + ?? this.manifest.find(item => item.href.includes('cover') + && item.mediaType.startsWith('image')) this.cfis = CFI.fromElements($$itemref) } @@ -859,7 +861,7 @@ class Loader { const h = window?.innerHeight ?? 600 return replacedImports // unprefix as most of the props are (only) supported unprefixed - .replace(/(?<=[{\s;])-epub-/gi, '') + .replace(/([{\s;])-epub-/gi, '$1') // 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') diff --git a/fixed-layout.js b/fixed-layout.js index b2adeba..b648a0d 100644 --- a/fixed-layout.js +++ b/fixed-layout.js @@ -1,3 +1,5 @@ +import 'construct-style-sheets-polyfill' + const parseViewport = str => str ?.split(/[,;\s]/) // NOTE: technically, only the comma is valid ?.filter(x => x) diff --git a/package-lock.json b/package-lock.json index 2341a20..a4fc3c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "foliate-js", "version": "0.0.0", "license": "MIT", + "dependencies": { + "construct-style-sheets-polyfill": "^3.1.0" + }, "devDependencies": { "@eslint/js": "^9.9.1", "@rollup/plugin-node-resolve": "^15.2.3", @@ -596,6 +599,12 @@ "license": "ISC", "optional": true }, + "node_modules/construct-style-sheets-polyfill": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.1.0.tgz", + "integrity": "sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/package.json b/package.json index e457e98..2ca62bf 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,8 @@ "dictd", "stardict", "opds" - ] + ], + "dependencies": { + "construct-style-sheets-polyfill": "^3.1.0" + } } diff --git a/paginator.js b/paginator.js index 0dd424b..babe551 100644 --- a/paginator.js +++ b/paginator.js @@ -250,7 +250,9 @@ class View { // it needs to be visible for Firefox to get computed style this.#iframe.style.display = 'block' const { vertical, rtl } = getDirection(doc) - const background = getBackground(doc) + this.docBackground = getBackground(doc) + doc.body.style.background = 'none' + const background = this.docBackground this.#iframe.style.display = 'none' this.#vertical = vertical @@ -610,7 +612,7 @@ export class Paginator extends HTMLElement { this.#mediaQueryListener = () => { if (!this.#view) return - this.#background.style.background = getBackground(this.#view.document) + this.#replaceBackground(this.#view.docBackground, this.columnCount) } this.#mediaQuery.addEventListener('change', this.#mediaQueryListener) } @@ -648,15 +650,31 @@ export class Paginator extends HTMLElement { this.#container.append(this.#view.element) return this.#view } + #replaceBackground(background, columnCount) { + const doc = this.#view?.document + const htmlStyle = doc.defaultView.getComputedStyle(doc.documentElement) + const themeBgColor = htmlStyle.getPropertyValue('--theme-bg-color') + if (background && themeBgColor) { + const parsedBackground = background.split(/\s(?=(?:url|rgb|hsl|#[0-9a-fA-F]{3,6}))/) + parsedBackground[0] = themeBgColor + background = parsedBackground.join(' ') + } + this.#background.innerHTML = '' + this.#background.style.display = 'grid' + this.#background.style.gridTemplateColumns = `repeat(${columnCount}, 1fr)` + for (let i = 0; i < columnCount; i++) { + const column = document.createElement('div') + column.style.background = background + column.style.width = '100%' + column.style.height = '100%' + this.#background.appendChild(column) + } + } #beforeRender({ vertical, rtl, background }) { this.#vertical = vertical this.#rtl = rtl this.#top.classList.toggle('vertical', vertical) - // set background to `doc` background - // this is needed because the iframe does not fill the whole element - this.#background.style.background = background - const { width, height } = this.#container.getBoundingClientRect() const size = vertical ? height : width @@ -705,6 +723,11 @@ export class Paginator extends HTMLElement { const columnWidth = (size / divisor) - gap this.setAttribute('dir', rtl ? 'rtl' : 'ltr') + // set background to `doc` background + // this is needed because the iframe does not fill the whole element + this.columnCount = divisor + this.#replaceBackground(background, this.columnCount) + const marginalDivisor = vertical ? Math.min(2, Math.ceil(width / maxInlineSize)) : divisor @@ -1042,11 +1065,11 @@ export class Paginator extends HTMLElement { if (shouldGo || !this.hasAttribute('animated')) await wait(100) this.#locked = false } - prev(distance) { - return this.#turnPage(-1, distance) + async prev(distance) { + return await this.#turnPage(-1, distance) } - next(distance) { - return this.#turnPage(1, distance) + async next(distance) { + return await this.#turnPage(1, distance) } prevSection() { return this.goTo({ index: this.#adjacentIndex(-1) }) @@ -1082,8 +1105,9 @@ export class Paginator extends HTMLElement { } else $style.textContent = styles // NOTE: needs `requestAnimationFrame` in Chromium - requestAnimationFrame(() => - this.#background.style.background = getBackground(this.#view.document)) + requestAnimationFrame(() => { + this.#replaceBackground(this.#view.docBackground, this.columnCount) + }) // needed because the resize observer doesn't work in Firefox this.#view?.document?.fonts?.ready?.then(() => this.#view.expand()) diff --git a/pdf.js b/pdf.js index 5abf583..32d0fa4 100644 --- a/pdf.js +++ b/pdf.js @@ -1,16 +1,13 @@ -const pdfjsPath = path => new URL(`vendor/pdfjs/${path}`, import.meta.url).toString() +const pdfjsPath = path => `/vendor/pdfjs/${path}` -import './vendor/pdfjs/pdf.mjs' +import '@pdfjs/pdf.mjs' const pdfjsLib = globalThis.pdfjsLib -pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath('pdf.worker.mjs') +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath('pdf.worker.min.mjs') const fetchText = async url => await (await fetch(url)).text() -// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.css -const textLayerBuilderCSS = await fetchText(pdfjsPath('text_layer_builder.css')) - -// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/annotation_layer_builder.css -const annotationLayerBuilderCSS = await fetchText(pdfjsPath('annotation_layer_builder.css')) +let textLayerBuilderCSS = null +let annotationLayerBuilderCSS = null const render = async (page, doc, zoom) => { const scale = zoom * devicePixelRatio @@ -77,6 +74,14 @@ const renderPage = async (page, getImageBlob) => { await page.render({ canvasContext, viewport }).promise return new Promise(resolve => canvas.toBlob(resolve)) } + // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.css + if (textLayerBuilderCSS == null) { + textLayerBuilderCSS = await fetchText(pdfjsPath('text_layer_builder.css')) + } + // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/annotation_layer_builder.css + if (annotationLayerBuilderCSS == null) { + annotationLayerBuilderCSS = await fetchText(pdfjsPath('annotation_layer_builder.css')) + } const src = URL.createObjectURL(new Blob([` diff --git a/text-walker.js b/text-walker.js index 3e2c44e..62518aa 100644 --- a/text-walker.js +++ b/text-walker.js @@ -32,7 +32,7 @@ export const textWalker = function* (x, func) { const walker = document.createTreeWalker(root, filter, { acceptNode }) const walk = x.commonAncestorContainer ? walkRange : walkDocument const nodes = walk(x, walker) - const strs = nodes.map(node => node.nodeValue) + const strs = nodes.map(node => node.nodeValue ?? '') const makeRange = (startIndex, startOffset, endIndex, endOffset) => { const range = document.createRange() range.setStart(nodes[startIndex], startOffset) diff --git a/tts.js b/tts.js index 0089ed3..9a695af 100644 --- a/tts.js +++ b/tts.js @@ -25,7 +25,7 @@ const getSegmenter = (lang = 'en', granularity = 'word') => { const segmenter = new Intl.Segmenter(lang, { granularity }) const granularityIsWord = granularity === 'word' return function* (strs, makeRange) { - const str = strs.join('') + const str = strs.join('').replace(/\r\n/g, ' ').replace(/\r/g, ' ').replace(/\n/g, ' ') let name = 0 let strIndex = -1 let sum = 0 @@ -34,10 +34,10 @@ const getSegmenter = (lang = 'en', granularity = 'word') => { while (sum <= index) sum += strs[++strIndex].length const startIndex = strIndex const startOffset = index - (sum - strs[strIndex].length) - const end = index + segment.length + const end = index + segment.length - 1 if (end < str.length) while (sum <= end) sum += strs[++strIndex].length const endIndex = strIndex - const endOffset = end - (sum - strs[strIndex].length) + const endOffset = end - (sum - strs[strIndex].length) + 1 yield [(name++).toString(), makeRange(startIndex, startOffset, endIndex, endOffset)] } @@ -207,11 +207,11 @@ export class TTS { #ranges #lastMark #serializer = new XMLSerializer() - constructor(doc, textWalker, highlight) { + constructor(doc, textWalker, highlight, granularity) { this.doc = doc this.highlight = highlight this.#list = new ListIterator(getBlocks(doc), range => { - const { entries, ssml } = getFragmentWithMarks(range, textWalker) + const { entries, ssml } = getFragmentWithMarks(range, textWalker, granularity) this.#ranges = new Map(entries) return [ssml, range] }) diff --git a/view.js b/view.js index 17566ac..55c818f 100644 --- a/view.js +++ b/view.js @@ -577,12 +577,12 @@ export class View extends HTMLElement { for (const item of list) this.deleteAnnotation(item) this.#searchResults.clear() } - async initTTS() { + async initTTS(granularity = 'word') { const doc = this.renderer.getContents()[0].doc if (this.tts && this.tts.doc === doc) return const { TTS } = await import('./tts.js') this.tts = new TTS(doc, textWalker, range => - this.renderer.scrollToAnchor(range, true)) + this.renderer.scrollToAnchor(range, true), granularity) } startMediaOverlay() { const { index } = this.renderer.getContents()[0]