UNPKG

vue-book-reader

Version:

<div align="center"> <img width=250 src="https://raw.githubusercontent.com/jinhuan138/vue--book-reader/master/public/logo.png" /> <h1>VueReader</h1> </div>

316 lines (315 loc) 9.13 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const NS = { XML: "http://www.w3.org/XML/1998/namespace", SSML: "http://www.w3.org/2001/10/synthesis" }; const blockTags = /* @__PURE__ */ new Set([ "article", "aside", "audio", "blockquote", "caption", "details", "dialog", "div", "dl", "dt", "dd", "figure", "footer", "form", "figcaption", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", "main", "math", "nav", "ol", "p", "pre", "section", "tr" ]); const getLang = (el) => { var _a; const x = el.lang || ((_a = el == null ? void 0 : el.getAttributeNS) == null ? void 0 : _a.call(el, NS.XML, "lang")); return x ? x : el.parentElement ? getLang(el.parentElement) : null; }; const getAlphabet = (el) => { var _a; const x = (_a = el == null ? void 0 : el.getAttributeNS) == null ? void 0 : _a.call(el, NS.XML, "lang"); return x ? x : el.parentElement ? getAlphabet(el.parentElement) : null; }; 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(""); let name = 0; let strIndex = -1; let sum = 0; for (const { index, segment, isWordLike } of segmenter.segment(str)) { if (granularityIsWord && !isWordLike) continue; while (sum <= index) sum += strs[++strIndex].length; const startIndex = strIndex; const startOffset = index - (sum - strs[strIndex].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) + 1; yield [ (name++).toString(), makeRange(startIndex, startOffset, endIndex, endOffset) ]; } }; }; const fragmentToSSML = (fragment, inherited) => { const ssml = document.implementation.createDocument(NS.SSML, "speak"); const { lang } = inherited; if (lang) ssml.documentElement.setAttributeNS(NS.XML, "lang", lang); const convert = (node, parent, inheritedAlphabet) => { if (!node) return; if (node.nodeType === 3) return ssml.createTextNode(node.textContent); if (node.nodeType === 4) return ssml.createCDATASection(node.textContent); if (node.nodeType !== 1) return; let el; const nodeName = node.nodeName.toLowerCase(); if (nodeName === "foliate-mark") { el = ssml.createElementNS(NS.SSML, "mark"); el.setAttribute("name", node.dataset.name); } else if (nodeName === "br") el = ssml.createElementNS(NS.SSML, "break"); else if (nodeName === "em" || nodeName === "strong") el = ssml.createElementNS(NS.SSML, "emphasis"); const lang2 = node.lang || node.getAttributeNS(NS.XML, "lang"); if (lang2) { if (!el) el = ssml.createElementNS(NS.SSML, "lang"); el.setAttributeNS(NS.XML, "lang", lang2); } const alphabet = node.getAttributeNS(NS.SSML, "alphabet") || inheritedAlphabet; if (!el) { const ph = node.getAttributeNS(NS.SSML, "ph"); if (ph) { el = ssml.createElementNS(NS.SSML, "phoneme"); if (alphabet) el.setAttribute("alphabet", alphabet); el.setAttribute("ph", ph); } } if (!el) el = parent; let child = node.firstChild; while (child) { const childEl = convert(child, el, alphabet); if (childEl && el !== childEl) el.append(childEl); child = child.nextSibling; } return el; }; convert(fragment.firstChild, ssml.documentElement, inherited.alphabet); return ssml; }; const getFragmentWithMarks = (range, textWalker, granularity) => { const lang = getLang(range.commonAncestorContainer); const alphabet = getAlphabet(range.commonAncestorContainer); const segmenter = getSegmenter(lang, granularity); const fragment = range.cloneContents(); const entries = [...textWalker(range, segmenter)]; const fragmentEntries = [...textWalker(fragment, segmenter)]; for (const [name, range2] of fragmentEntries) { const mark = document.createElement("foliate-mark"); mark.dataset.name = name; range2.insertNode(mark); } const ssml = fragmentToSSML(fragment, { lang, alphabet }); return { entries, ssml }; }; const rangeIsEmpty = (range) => !range.toString().trim(); function* getBlocks(doc) { let last; const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT); for (let node = walker.nextNode(); node; node = walker.nextNode()) { const name = node.tagName.toLowerCase(); if (blockTags.has(name)) { if (last) { last.setEndBefore(node); if (!rangeIsEmpty(last)) yield last; } last = doc.createRange(); last.setStart(node, 0); } } if (!last) { last = doc.createRange(); last.setStart(doc.body.firstChild ?? doc.body, 0); } last.setEndAfter(doc.body.lastChild ?? doc.body); if (!rangeIsEmpty(last)) yield last; } class ListIterator { #arr = []; #iter; #index = -1; #f; constructor(iter, f = (x) => x) { this.#iter = iter; this.#f = f; } current() { if (this.#arr[this.#index]) return this.#f(this.#arr[this.#index]); } first() { const newIndex = 0; if (this.#arr[newIndex]) { this.#index = newIndex; return this.#f(this.#arr[newIndex]); } } prev() { const newIndex = this.#index - 1; if (this.#arr[newIndex]) { this.#index = newIndex; return this.#f(this.#arr[newIndex]); } } next() { const newIndex = this.#index + 1; if (this.#arr[newIndex]) { this.#index = newIndex; return this.#f(this.#arr[newIndex]); } while (true) { const { done, value } = this.#iter.next(); if (done) break; this.#arr.push(value); if (this.#arr[newIndex]) { this.#index = newIndex; return this.#f(this.#arr[newIndex]); } } } find(f) { const index = this.#arr.findIndex((x) => f(x)); if (index > -1) { this.#index = index; return this.#f(this.#arr[index]); } while (true) { const { done, value } = this.#iter.next(); if (done) break; this.#arr.push(value); if (f(value)) { this.#index = this.#arr.length - 1; return this.#f(value); } } } } class TTS { #list; #ranges; #lastMark; #serializer = new XMLSerializer(); constructor(doc, textWalker, highlight, granularity) { this.doc = doc; this.highlight = highlight; this.#list = new ListIterator(getBlocks(doc), (range) => { const { entries, ssml } = getFragmentWithMarks(range, textWalker, granularity); this.#ranges = new Map(entries); return [ssml, range]; }); } #getMarkElement(doc, mark) { if (!mark) return null; return doc.querySelector(`mark[name="${CSS.escape(mark)}"`); } #speak(doc, getNode) { var _a, _b; if (!doc) return; if (!getNode) return this.#serializer.serializeToString(doc); const ssml = document.implementation.createDocument(NS.SSML, "speak"); ssml.documentElement.replaceWith(ssml.importNode(doc.documentElement, true)); let node = (_a = getNode(ssml)) == null ? void 0 : _a.previousSibling; while (node) { const next = node.previousSibling ?? ((_b = node.parentNode) == null ? void 0 : _b.previousSibling); node.parentNode.removeChild(node); node = next; } return this.#serializer.serializeToString(ssml); } start() { this.#lastMark = null; const [doc] = this.#list.first() ?? []; if (!doc) return this.next(); return this.#speak(doc, (ssml) => this.#getMarkElement(ssml, this.#lastMark)); } resume() { const [doc] = this.#list.current() ?? []; if (!doc) return this.next(); return this.#speak(doc, (ssml) => this.#getMarkElement(ssml, this.#lastMark)); } prev(paused) { this.#lastMark = null; const [doc, range] = this.#list.prev() ?? []; if (paused && range) this.highlight(range.cloneRange()); return this.#speak(doc); } next(paused) { this.#lastMark = null; const [doc, range] = this.#list.next() ?? []; if (paused && range) this.highlight(range.cloneRange()); return this.#speak(doc); } from(range) { this.#lastMark = null; const [doc] = this.#list.find((range_) => range.compareBoundaryPoints(Range.END_TO_START, range_) <= 0); let mark; for (const [name, range_] of this.#ranges.entries()) if (range.compareBoundaryPoints(Range.START_TO_START, range_) <= 0) { mark = name; break; } return this.#speak(doc, (ssml) => this.#getMarkElement(ssml, mark)); } setMark(mark) { const range = this.#ranges.get(mark); if (range) { this.#lastMark = mark; this.highlight(range.cloneRange()); } } } exports.TTS = TTS;