Skip to content

[WIP] Merge enhancements and fixes from Readest project into foliate-js upstream #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
20 changes: 17 additions & 3 deletions epub.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const childGetter = (doc, ns) => {

const resolveURL = (url, relativeTo) => {
try {
if (relativeTo.includes(':')) return new URL(url, relativeTo)
if (relativeTo.includes(':') && !relativeTo.startsWith('OEBPS')) return new URL(url, relativeTo)
// the base needs to be a valid URL, so set a base URL and then remove it
const root = 'https://invalid.invalid/'
const obj = new URL(url, root + relativeTo)
Expand Down Expand Up @@ -203,7 +203,7 @@ const getMetadata = opf => {
if (!els) return null
return Object.groupBy(els.map(parse), x => x.property)
}
const dc = Object.fromEntries(Object.entries(Object.groupBy(els.dc, el => el.localName))
const dc = Object.fromEntries(Object.entries(Object.groupBy(els.dc || [], el => el.localName))
.map(([name, els]) => [name, els.map(parse)]))
const properties = getProperties() ?? {}
const legacyMeta = Object.fromEntries(els.legacyMeta?.map(el =>
Expand Down Expand Up @@ -669,6 +669,8 @@ class Resources {
?? this.getItemByID($$$(opf, 'meta')
.find(filterAttribute('name', 'cover'))
?.getAttribute('content'))
?? this.manifest.find(item => item.href.includes('cover')
&& item.mediaType.startsWith('image'))
?? this.getItemByHref(this.guide
?.find(ref => ref.type.includes('cover'))?.href)

Expand Down Expand Up @@ -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')
Expand All @@ -868,6 +870,18 @@ class Loader {
`-webkit-column-break-${x}:`)
.replace(/break-(after|before|inside)\s*:\s*(avoid-)?page/gi, (_, x, y) =>
`break-${x}: ${y ?? ''}column`)
// replace absolute font sizes with rem units
.replace(/font-size\s*:\s*xx-small/gi, 'font-size: 0.6rem')
.replace(/font-size\s*:\s*x-small/gi, 'font-size: 0.75rem')
.replace(/font-size\s*:\s*small/gi, 'font-size: 0.875rem')
.replace(/font-size\s*:\s*medium/gi, 'font-size: 1rem')
.replace(/font-size\s*:\s*large/gi, 'font-size: 1.2rem')
.replace(/font-size\s*:\s*x-large/gi, 'font-size: 1.5rem')
.replace(/font-size\s*:\s*xx-large/gi, 'font-size: 2rem')
.replace(/font-size\s*:\s*xxx-large/gi, 'font-size: 3rem')
// replace hardcoded colors
.replace(/color\s*:\s*#000000/gi, 'color: unset')
.replace(/color\s*:\s*#000/gi, 'color: unset')
}
// find & replace all possible relative paths for all assets without parsing
replaceString(str, href, parents = []) {
Expand Down
2 changes: 2 additions & 0 deletions fixed-layout.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
20 changes: 14 additions & 6 deletions footnotes.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,18 @@ export class FootnoteHandler extends EventTarget {
const type = getReferencedType(el)
const hidden = el?.matches?.('aside') && type === 'footnote'
if (el) {
const range = el.startContainer ? el : doc.createRange()
if (!el.startContainer) {
if (el.matches('li, aside')) range.selectNodeContents(el)
else range.selectNode(el)
let range
if (el.startContainer) {
range = el
} else if (el.matches('li, aside')) {
range = doc.createRange()
range.selectNodeContents(el)
} else if (el.closest('li')) {
range = doc.createRange()
range.selectNodeContents(el.closest('li'))
} else {
range = doc.createRange()
range.selectNode(el)
}
const frag = range.extractContents()
doc.body.replaceChildren()
Expand All @@ -85,9 +93,9 @@ export class FootnoteHandler extends EventTarget {
})
}
handle(book, e) {
const { a, href } = e.detail
const { a, href, follow } = e.detail
const { yes, maybe } = isFootnoteReference(a)
if (yes) {
if (yes || follow) {
e.preventDefault()
return Promise.resolve(book.resolveHref(href)).then(target =>
this.#showFragment(book, target, href))
Expand Down
60 changes: 57 additions & 3 deletions overlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const createSVGElement = tag =>
export class Overlayer {
#svg = createSVGElement('svg')
#map = new Map()
constructor() {
#doc = null
constructor(doc) {
this.#doc = doc
Object.assign(this.#svg.style, {
position: 'absolute', top: '0', left: '0',
width: '100%', height: '100%',
Expand All @@ -14,10 +16,50 @@ export class Overlayer {
get element() {
return this.#svg
}
get #zoom() {
// Safari does not zoom the client rects, while Chrome, Edge and Firefox does
if (/^((?!chrome|android).)*AppleWebKit/i.test(navigator.userAgent) && !window.chrome) {
return window.getComputedStyle(this.#doc.body).zoom || 1.0
}
return 1.0
}
#splitRangeByParagraph(range) {
const ancestor = range.commonAncestorContainer
const paragraphs = Array.from(ancestor.querySelectorAll?.('p') || [])
if (paragraphs.length === 0) return [range]

const splitRanges = []
paragraphs.forEach((p) => {
const pRange = document.createRange()
if (range.intersectsNode(p)) {
pRange.selectNodeContents(p)
if (pRange.compareBoundaryPoints(Range.START_TO_START, range) < 0) {
pRange.setStart(range.startContainer, range.startOffset)
}
if (pRange.compareBoundaryPoints(Range.END_TO_END, range) > 0) {
pRange.setEnd(range.endContainer, range.endOffset)
}
splitRanges.push(pRange)
}
})
return splitRanges
}
add(key, range, draw, options) {
if (this.#map.has(key)) this.remove(key)
if (typeof range === 'function') range = range(this.#svg.getRootNode())
const rects = range.getClientRects()
const zoom = this.#zoom
let rects = []
this.#splitRangeByParagraph(range).forEach((pRange) => {
const pRects = Array.from(pRange.getClientRects()).map(rect => ({
left: rect.left * zoom,
top: rect.top * zoom,
right: rect.right * zoom,
bottom: rect.bottom * zoom,
width: rect.width * zoom,
height: rect.height * zoom,
}))
rects = rects.concat(pRects)
})
const element = draw(rects, options)
this.#svg.append(element)
this.#map.set(key, { range, draw, options, element, rects })
Expand All @@ -31,7 +73,19 @@ export class Overlayer {
for (const obj of this.#map.values()) {
const { range, draw, options, element } = obj
this.#svg.removeChild(element)
const rects = range.getClientRects()
const zoom = this.#zoom
let rects = []
this.#splitRangeByParagraph(range).forEach((pRange) => {
const pRects = Array.from(pRange.getClientRects()).map(rect => ({
left: rect.left * zoom,
top: rect.top * zoom,
right: rect.right * zoom,
bottom: rect.bottom * zoom,
width: rect.width * zoom,
height: rect.height * zoom,
}))
rects = rects.concat(pRects)
})
const el = draw(rects, options)
this.#svg.append(el)
obj.element = el
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
"dictd",
"stardict",
"opds"
]
],
"dependencies": {
"construct-style-sheets-polyfill": "^3.1.0"
}
}
Loading