-
-
Notifications
You must be signed in to change notification settings - Fork 62
/
footnotes.js
103 lines (98 loc) · 4.06 KB
/
footnotes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const getTypes = el => new Set(el?.getAttributeNS?.('http://www.idpf.org/2007/ops', 'type')?.split(' '))
const getRoles = el => new Set(el?.getAttribute?.('role')?.split(' '))
const isSuper = el => {
if (el.matches('sup')) return true
const { verticalAlign } = getComputedStyle(el)
return verticalAlign === 'super'
|| verticalAlign === 'top'
|| verticalAlign === 'text-top'
|| /^\d/.test(verticalAlign)
}
const refTypes = ['biblioref', 'glossref', 'noteref']
const refRoles = ['doc-biblioref', 'doc-glossref', 'doc-noteref']
const isFootnoteReference = a => {
const types = getTypes(a)
const roles = getRoles(a)
return {
yes: refRoles.some(r => roles.has(r)) || refTypes.some(t => types.has(t)),
maybe: () => !types.has('backlink') && !roles.has('doc-backlink')
&& (isSuper(a) || a.children.length === 1 && isSuper(a.children[0])
|| isSuper(a.parentElement)),
}
}
const getReferencedType = el => {
const types = getTypes(el)
const roles = getRoles(el)
return roles.has('doc-biblioentry') || types.has('biblioentry') ? 'biblioentry'
: roles.has('definition') || types.has('glossdef') ? 'definition'
: roles.has('doc-endnote') || types.has('endnote') || types.has('rearnote') ? 'endnote'
: roles.has('doc-footnote') || types.has('footnote') ? 'footnote'
: roles.has('note') || types.has('note') ? 'note' : null
}
const isInline = 'a, span, sup, sub, em, strong, i, b, small, big'
const extractFootnote = (doc, anchor) => {
let el = anchor(doc)
const target = el
while (el.matches(isInline)) {
const parent = el.parentElement
if (!parent) break
el = parent
}
if (el === doc.body) {
const sibling = target.nextElementSibling
if (sibling && !sibling.matches(isInline)) return sibling
throw new Error('Failed to extract footnote')
}
return el
}
export class FootnoteHandler extends EventTarget {
detectFootnotes = true
#showFragment(book, { index, anchor }, href) {
const view = document.createElement('foliate-view')
return new Promise((resolve, reject) => {
view.addEventListener('load', e => {
try {
const { doc } = e.detail
const el = anchor(doc)
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)
}
const frag = range.extractContents()
doc.body.replaceChildren()
doc.body.appendChild(frag)
}
const detail = { view, href, type, hidden, target: el }
this.dispatchEvent(new CustomEvent('render', { detail }))
resolve()
} catch (e) {
reject(e)
}
})
view.open(book)
.then(() => this.dispatchEvent(new CustomEvent('before-render', { detail: { view } })))
.then(() => view.goTo(index))
.catch(reject)
})
}
handle(book, e) {
const { a, href } = e.detail
const { yes, maybe } = isFootnoteReference(a)
if (yes) {
e.preventDefault()
return Promise.resolve(book.resolveHref(href)).then(target =>
this.#showFragment(book, target, href))
}
else if (this.detectFootnotes && maybe()) {
e.preventDefault()
return Promise.resolve(book.resolveHref(href)).then(({ index, anchor }) => {
const target = { index, anchor: doc => extractFootnote(doc, anchor) }
return this.#showFragment(book, target, href)
})
}
}
}