diff --git a/src/assets/lib/kookit.min.js b/src/assets/lib/kookit.min.js index 1181fb3e..94feef3f 100644 --- a/src/assets/lib/kookit.min.js +++ b/src/assets/lib/kookit.min.js @@ -1,18839 +1 @@ -import _ from 'underscore'; -import rangy from 'rangy/lib/rangy-core.js'; -import 'rangy/lib/rangy-textrange'; -import JSZip from 'jszip'; -import { unzlibSync } from 'fflate'; -import chardet from 'chardet'; -import untar from 'js-untar'; -import mammoth from 'mammoth'; -import { marked } from 'marked'; -import mhtml2html from 'mhtml2html'; - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ - -function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -} - -const convertStyleNum = (value) => { - if (!value) - return 0; - return parseFloat(value + ""); -}; -const convertComputedNum = (value) => { - return parseFloat(value.substring(0, value.length - 2)); -}; -const handleIframeHeight = (element, readerMode, format, iframe, doc) => __awaiter(void 0, void 0, void 0, function* () { - yield Promise.race([ - Promise.all(Array.from([...doc.images, ...doc.querySelectorAll("image")]).map((img) => { - if (img.complete) - return Promise.resolve(img.naturalHeight !== 0); - return new Promise((resolve) => { - img.addEventListener("load", () => resolve(true)); - img.addEventListener("error", () => resolve(false)); - }); - })), - new Promise((resolve, reject) => { - setTimeout(() => { - // reject(new Error("Timeout")); - resolve("image load timeout"); - }, 10); - }), - ]); - yield handleImageSize(element, readerMode, format, doc); - handleTextStyle(doc); - if (readerMode !== "scroll") { - iframe.height = element.clientHeight + "px"; - if (readerMode === "double") { - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let pageWidth = (element.clientWidth + gap) / 2; - if (((doc.body.scrollWidth - doc.body.clientWidth) / pageWidth) % 2 === - 1) { - let tailElem = document.createElement("div"); - tailElem.setAttribute("style", "height: " + - doc.body.clientHeight + - "px; display: inline-block; width: " + - (pageWidth - gap) + - "px"); - doc.body.appendChild(tailElem); - } - } - } - else { - //fix text blocked issue under scroll readerMode, don't ask me why - iframe.height = doc.body.scrollHeight + "px"; - iframe.height = doc.body.scrollHeight + 300 + "px"; - } - // await new Promise((r) => setTimeout(r, 1)); -}); -const handleOneChapterDoc = (item, isSearch) => __awaiter(void 0, void 0, void 0, function* () { - let chapterText = ""; - if (item && item.load) { - let blob = yield fetch(yield item.load()).then((r) => r.blob()); - chapterText = yield blob.text(); - } - if (isSearch) { - return chapterText; - } - if (item && item.loadAsset) { - chapterText = yield handlePrecacheAssets(chapterText, item.loadAsset); - } - chapterText = handleImageMarker(chapterText); - return chapterText; -}); -const getImageElement = (Element) => { - return Array.from(Element.querySelectorAll("img, image")); -}; -const handlePrecacheAssets = (bookStr, loadAsset) => __awaiter(void 0, void 0, void 0, function* () { - let chapterDoc = new DOMParser().parseFromString(bookStr, "text/html"); - let imgDomList = getImageElement(chapterDoc); - for (let subindex = 0; subindex < imgDomList.length; subindex++) { - if (imgDomList[subindex].getAttribute("src")) { - imgDomList[subindex].src = yield loadAsset(imgDomList[subindex].getAttribute("src")); - } - else if (imgDomList[subindex].getAttribute("xlink:href")) { - imgDomList[subindex].setAttribute("xlink:href", yield loadAsset(imgDomList[subindex].getAttribute("xlink:href"))); - } - } - let linkList = Array.from(chapterDoc.getElementsByTagName("link")); - for (let index = 0; index < linkList.length; index++) { - const link = linkList[index]; - if (link.getAttribute("href")) { - link.href = yield loadAsset(link.getAttribute("href")); - } - } - return chapterDoc.documentElement.innerHTML; -}); -const handleImageMarker = (bookStr) => { - let chapterDoc = new DOMParser().parseFromString(bookStr, "text/html"); - let imgDomList = getImageElement(chapterDoc); - if (imgDomList.length === 0) { - return bookStr; - } - else { - for (let i = 0; i < imgDomList.length; i++) { - if (imgDomList[i].tagName === "image") { - continue; - } - var newItem = document.createElement("kookitmarker"); - var textnode = document.createTextNode("img"); - newItem.appendChild(textnode); - newItem.setAttribute("style", "visibility: hidden; position: absolute;display: inline-block; width: 0; height: 0;"); - // 找到图片元素在body中的位置,确保marker插入到body下 - let imgElement = imgDomList[i]; - // 找到包含当前图片的顶级body子元素 - let topLevelParent = imgElement; - while (topLevelParent.parentElement && - topLevelParent.parentElement !== chapterDoc.body) { - topLevelParent = topLevelParent.parentElement; - } - // 在该顶级元素后插入marker - if (topLevelParent.parentElement === chapterDoc.body) { - topLevelParent.insertAdjacentElement("afterend", newItem); - } - else { - // 如果找不到合适位置,插入到body末尾 - chapterDoc.body.appendChild(newItem); - } - } - return chapterDoc.documentElement.innerHTML; - } -}; -const createIframe = (element, scale) => { - var iframe = document.createElement("iframe"); - iframe.style.width = scale ? (scale - 0.4) * 100 + "%" : "100%"; - iframe.style.margin = "0"; - iframe.style.border = "0"; - iframe.style.padding = "0"; - iframe.style.minHeight = "calc(100% - 2px)"; - iframe.style.fontSize = "100%"; - iframe.style.font = "inherit"; - iframe.scrolling = "no"; - iframe.tabIndex = 0; - iframe.id = "kookit-iframe"; - iframe.style.verticalAlign = "baseline"; - element.innerHTML = ""; - element.appendChild(iframe); - // 控制iframe滚动到页面水平正中的位置 - if (scale) { - element.scrollLeft = element.scrollWidth / 2 - element.clientWidth / 2; - } -}; -const progressInfo = (readerMode, doc, element) => { - //TODO 是否有必要保留延时 - // if (parseInt(doc.body.scrollWidth / doc.body.clientWidth + "") === 1) { - // await new Promise((r) => setTimeout(r, 1000)); - // } - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - return { - totalPage: readerMode === "scroll" - ? Math.floor(element.scrollHeight / (element.clientHeight - 50)) - : readerMode === "single" - ? Math.round(parseFloat(doc.body.scrollWidth / (doc.body.clientWidth + gap) + "")) - : Math.round(parseFloat(doc.body.scrollWidth / (doc.body.clientWidth + gap) + "")) * 2, - currentPage: readerMode === "scroll" - ? Math.floor(element.scrollTop / (element.clientHeight - 50)) + 1 - : Math.round(parseFloat(convertStyleNum(doc.body.scrollLeft) / - (doc.body.clientWidth + gap) + - "")) + 1, - }; -}; -const handleTextStyle = (doc) => { - var _a; - let textNodes = doc.querySelectorAll("a, article, cite, div, li, p, span, pre, dt, dd, table, bold, font"); - for (let index = 0; index < textNodes.length; index++) { - const element = textNodes[index]; - if (element.className.indexOf("kookit-text") === -1 && - ((_a = element.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) !== "RT") { - element.className = element.className + " kookit-text"; - } - } - let titleNodes = doc.querySelectorAll("h1, h2, h3, h4, h5, h6, title"); - for (let index = 0; index < titleNodes.length; index++) { - const element = titleNodes[index]; - if (element.className.indexOf("kookit-title") === -1) { - element.className = "kookit-title " + element.className; - } - } -}; -const getImageMeta = (url) => __awaiter(void 0, void 0, void 0, function* () { - const img = new Image(); - img.src = url; - try { - yield img.decode(); - } - catch (error) { - console.error(error); - } - return img; -}); -const handleImageSize = (element, readerMode, format, doc) => __awaiter(void 0, void 0, void 0, function* () { - var _a, _b; - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let scale = readerMode === "double" ? 2 : 1; - (element.clientWidth - gap) / scale; - let imgs = doc.querySelectorAll("img, image"); - for (let item of imgs) { - let parentItem = item.parentElement; - let maxHeight = 0; - let maxWidth = 0; - let width = item.naturalWidth; - let height = item.naturalHeight; - if (item.tagName === "image") { - let img = yield getImageMeta(item.getAttribute("xlink:href")); - width = img.naturalWidth; - height = img.naturalHeight; - } - if (format.startsWith("CB") && readerMode === "scroll") { - maxWidth = parentItem.offsetWidth; - } - else if (format.startsWith("CB") && readerMode === "single") { - maxHeight = element.clientHeight; - maxWidth = element.clientWidth; - } - else if (parentItem && - width && - height && - parentItem.clientHeight && - parentItem.clientWidth) { - let isImageScaleLargerThanElement = height / width > parentItem.clientHeight / parentItem.clientWidth; - if (isImageScaleLargerThanElement) { - maxHeight = parentItem.clientHeight; - maxWidth = parseInt((maxHeight * width) / height + ""); - } - else { - maxWidth = parentItem.clientWidth; - maxHeight = parseInt((maxWidth * height) / width + ""); - } - if (maxHeight > doc.body.clientHeight && readerMode !== "scroll") { - maxWidth = parseInt(maxWidth * (doc.body.clientHeight / maxHeight) + ""); - maxHeight = doc.body.clientHeight; - } - parentItem.style.textIndent = "0px"; - } - else if (parentItem && - parentItem.clientWidth && - parentItem.clientWidth > 0) { - maxWidth = parentItem.clientWidth; - maxHeight = parentItem.clientHeight; - parentItem.style.textIndent = "0px"; - } - else { - maxWidth = element.clientWidth; - maxHeight = element.clientHeight; - } - if (maxWidth) { - maxWidth = Math.min(readerMode === "scroll" || readerMode === "single" - ? element.clientWidth - : (element.clientWidth - gap) / 2, maxWidth); - } - else { - maxWidth = - readerMode === "scroll" || readerMode === "single" - ? element.clientWidth - : (element.clientWidth - gap) / 2; - } - if (width && height) { - if (width > height) { - maxHeight = maxWidth * (height / width); - } - else { - if (maxHeight / maxWidth > height / width) { - maxHeight = maxWidth * (height / width); - } - else { - maxWidth = maxHeight * (width / height); - } - } - } - if (maxWidth || maxHeight) { - //轻易不要改这里,很容易出问题 - item.setAttribute("style", (item.getAttribute("style") ? item.getAttribute("style") : "") + - ";" + - `max-width: ${maxWidth > 0 ? maxWidth + "px" : ""};max-height:${maxHeight > 0 ? maxHeight + "px" : ""}; margin: 0 auto; min-width: 0px; min-height: 0px; ${format.startsWith("CB") - ? `margin-left: calc(100% - ${item.clientWidth}px);` - : ""}`); - } - if (item.tagName === "image") { - (_a = item.parentElement) === null || _a === void 0 ? void 0 : _a.setAttribute("width", maxWidth); - (_b = item.parentElement) === null || _b === void 0 ? void 0 : _b.setAttribute("height", maxHeight); - } - if (format.startsWith("CB") && readerMode === "scroll") { - item.setAttribute("style", (item.getAttribute("style") ? item.getAttribute("style") : "") + - ";margin-left: 0px; width: 100%;"); - } - if (format.startsWith("CB") && readerMode !== "scroll") { - item.setAttribute("style", (item.getAttribute("style") ? item.getAttribute("style") : "") + - `;margin-left: calc(50% - ${item.getBoundingClientRect().width / 2}px);`); - } - } -}); -const handleLayout = (element, readerMode, doc) => { - let style = doc.createElement("style"); - style.id = "default-style"; - style.textContent = - "p,empty-line{display: inherit;margin-block-start: inherit;margin-block-end: inherit;margin-inline-start: inherit;margin-inline-end: inherit;}body{margin: 0px}"; - doc.head.appendChild(style); - if (readerMode === "scroll") { - return; - } - let scale = readerMode === "double" ? 2 : 1; - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - doc.body.setAttribute("style", `width: ${element.clientWidth + "px"};height: 100%;overflow-y: hidden;overflow-X: hidden;padding-left: 0px;padding-right: 0px;margin: 0px;box-sizing: border-box;touch-action:none; overscroll-behavior: none;max-width: inherit;column-fill: auto;column-gap: ${gap}px; column-width: ${(element.clientWidth - gap) / scale}px;`); -}; -function getSelectedElement(doc) { - const selection = doc.getSelection(); - if (!selection) - return null; - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - const selectedElement = range.startContainer.parentElement; - return selectedElement; - } - return null; -} - -const isString = (value) => { - return typeof value === "string" || value instanceof String; -}; -class GeneralParser { - constructor(book) { - this.book = book; - this.chapterList = []; - this.flattenChapters = []; - this.chapterDocList = []; - } - unescapeHtml(htmlStr) { - if (!htmlStr) - return ""; - const doc = new DOMParser().parseFromString(htmlStr, "text/html"); - return doc.documentElement.textContent || ""; - } - getChapter(toc) { - return __awaiter(this, void 0, void 0, function* () { - if (toc) { - this.chapterList = yield Promise.all(toc.map((item, index) => __awaiter(this, void 0, void 0, function* () { - let chapterIndex = index; - try { - chapterIndex = - item.href && (yield this.book.resolveHref(item.href)) - ? (yield this.book.resolveHref(item.href)).index - : chapterIndex; - } - catch (error) { - console.error(error); - } - return { - label: this.unescapeHtml(item.label) - ? this.unescapeHtml(item.label) - : chapterIndex + "", - href: item.href ? item.href : "title" + chapterIndex, - index: chapterIndex, - subitems: item.subitems ? yield this.getChapter(item.subitems) : [], - }; - }))); - } - else { - this.chapterList = yield Promise.all(this.book.sections.map((item, index) => __awaiter(this, void 0, void 0, function* () { - return { - label: this.unescapeHtml(item.label) - ? this.unescapeHtml(item.label) - : index + "", - href: item.href ? item.href : "title" + index, - index: index, - subitems: item.subitems ? yield this.getChapter(item.subitems) : [], - }; - }))); - } - this.flattenChapters = this.flatChapter(this.chapterList); - return this.chapterList; - }); - } - getChapterDoc() { - return __awaiter(this, void 0, void 0, function* () { - const chapterIndexList = this.flattenChapters.map((item) => item.index); - return this.book.sections.map((item, index) => { - if (chapterIndexList.indexOf(index) > -1) { - return { - label: this.unescapeHtml(this.flattenChapters[chapterIndexList.indexOf(index)].label), - href: this.flattenChapters[chapterIndexList.indexOf(index)].href, - text: item, - }; - } - else { - return { - label: "", - href: "", - text: item, - }; - } - }); - }); - } - flatChapter(chapters) { - let newChapter = []; - for (let i = 0; i < chapters.length; i++) { - if (chapters[i].subitems && chapters[i].subitems.length > 0) { - newChapter.push(chapters[i]); - newChapter = newChapter.concat(this.flatChapter(chapters[i].subitems)); - } - else { - newChapter.push(chapters[i]); - } - } - return newChapter; - } - getMetadata() { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - const metadata = this.book.metadata; - let author = metadata.author && - metadata.author[0] && - metadata.author[0].name && - isString(metadata.author[0].name) - ? metadata.author[0].name - : metadata.author && - metadata.author[0] && - isString(metadata.author[0]) - ? metadata.author[0] - : metadata.author && isString(metadata.author) - ? metadata.author - : ""; - try { - const blob = yield this.book.getCover(); - var reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => { - resolve(Object.assign(Object.assign({}, metadata), { name: metadata.title, author: author, description: metadata.description, publisher: metadata.publisher, cover: reader.result })); - }; - } - catch (error) { - console.error(error); - try { - resolve(Object.assign(Object.assign({}, metadata), { name: metadata.title, author: author, description: metadata.description, publisher: metadata.publisher, cover: "" })); - } - catch (error) { - console.error(error); - reject(error); - } - } - })); - } -} - -const findIndices = (arr, f) => arr - .map((x, i, a) => f(x, i, a) ? i : null).filter(x => x != null); -const splitAt = (arr, is) => [-1, ...is, arr.length].reduce(({ xs, a }, b) => - ({ xs: xs?.concat([arr.slice(a + 1, b)]) ?? [], a: b }), {}).xs; -const concatArrays = (a, b) => - a.slice(0, -1).concat([a[a.length - 1].concat(b[0])]).concat(b.slice(1)); - -const isNumber = /\d/; -const isCFI = /^epubcfi\((.*)\)$/; -const escapeCFI = str => str.replace(/[\^[\](),;=]/g, '^$&'); - -const wrap = x => isCFI.test(x) ? x : `epubcfi(${x})`; -const unwrap = x => x.match(isCFI)?.[1] ?? x; - -const tokenizer = str => { - const tokens = []; - let state, escape, value = ''; - const push = x => (tokens.push(x), state = null, value = ''); - const cat = x => (value += x, escape = false); - for (const char of Array.from(str.trim()).concat('')) { - if (char === '^' && !escape) { - escape = true; - continue - } - if (state === '!') push(['!']); - else if (state === ',') push([',']); - else if (state === '/' || state === ':') { - if (isNumber.test(char)) { - cat(char); - continue - } else push([state, parseInt(value)]); - } else if (state === '~') { - if (isNumber.test(char) || char === '.') { - cat(char); - continue - } else push(['~', parseFloat(value)]); - } else if (state === '@') { - if (char === ':') { - push(['@', parseFloat(value)]); - state = '@'; - continue - } - if (isNumber.test(char) || char === '.') { - cat(char); - continue - } else push(['@', parseFloat(value)]); - } else if (state === '[') { - if (char === ';' && !escape) { - push(['[', value]); - state = ';'; - } else if (char === ',' && !escape) { - push(['[', value]); - state = '['; - } else if (char === ']' && !escape) push(['[', value]); - else cat(char); - continue - } else if (state?.startsWith(';')) { - if (char === '=' && !escape) { - state = `;${value}`; - value = ''; - } else if (char === ';' && !escape) { - push([state, value]); - state = ';'; - } else if (char === ']' && !escape) push([state, value]); - else cat(char); - continue - } - if (char === '/' || char === ':' || char === '~' || char === '@' - || char === '[' || char === '!' || char === ',') state = char; - } - return tokens -}; - -const findTokens = (tokens, x) => findIndices(tokens, ([t]) => t === x); - -const parser = tokens => { - const parts = []; - let state; - for (const [type, val] of tokens) { - if (type === '/') parts.push({ index: val }); - else { - const last = parts[parts.length - 1]; - if (type === ':') last.offset = val; - else if (type === '~') last.temporal = val; - else if (type === '@') last.spatial = (last.spatial ?? []).concat(val); - else if (type === ';s') last.side = val; - else if (type === '[') { - if (state === '/' && val) last.id = val; - else { - last.text = (last.text ?? []).concat(val); - continue - } - } - } - state = type; - } - return parts -}; - -// split at step indirections, then parse each part -const parserIndir = tokens => - splitAt(tokens, findTokens(tokens, '!')).map(parser); - -const parse = cfi => { - const tokens = tokenizer(unwrap(cfi)); - const commas = findTokens(tokens, ','); - if (!commas.length) return parserIndir(tokens) - const [parent, start, end] = splitAt(tokens, commas).map(parserIndir); - return { parent, start, end } -}; - -const partToString = ({ index, id, offset, temporal, spatial, text, side }) => { - const param = side ? `;s=${side}` : ''; - return `/${index}` - + (id ? `[${escapeCFI(id)}${param}]` : '') - // "CFI expressions [..] SHOULD include an explicit character offset" - + (offset != null && index % 2 ? `:${offset}` : '') - + (temporal ? `~${temporal}` : '') - + (spatial ? `@${spatial.join(':')}` : '') - + (text || (!id && side) ? '[' - + (text?.map(escapeCFI)?.join(',') ?? '') - + param + ']' : '') -}; - -const toInnerString = parsed => parsed.parent - ? [parsed.parent, parsed.start, parsed.end].map(toInnerString).join(',') - : parsed.map(parts => parts.map(partToString).join('')).join('!'); - -const toString = parsed => wrap(toInnerString(parsed)); - -const collapse = (x, toEnd) => typeof x === 'string' - ? toString(collapse(parse(x), toEnd)) - : x.parent ? concatArrays(x.parent, x[toEnd ? 'end' : 'start']) : x; - -const isTextNode = ({ nodeType }) => nodeType === 3 || nodeType === 4; -const isElementNode = ({ nodeType }) => nodeType === 1; - -// child nodes are organized such that the result is always -// [element, text, element, text, ..., element], -// regardless of the actual structure in the document; -// so multiple text nodes need to be combined, and nonexistent ones counted; -// see "Step Reference to Child Element or Character Data (/)" in EPUB CFI spec -const indexChildNodes = node => { - const nodes = Array.from(node.childNodes) - // "content other than element and character data is ignored" - .filter(node => isTextNode(node) || isElementNode(node)) - .reduce((arr, node) => { - let last = arr[arr.length - 1]; - if (!last) arr.push(node); - // "there is one chunk between each pair of child elements" - else if (isTextNode(node)) { - if (Array.isArray(last)) last.push(node); - else if (isTextNode(last)) arr[arr.length - 1] = [last, node]; - else arr.push(node); - } else { - if (isElementNode(last)) arr.push(null, node); - else arr.push(node); - } - return arr - }, []); - // "the first chunk is located before the first child element" - if (isElementNode(nodes[0])) nodes.unshift('first'); - // "the last chunk is located after the last child element" - if (isElementNode(nodes[nodes.length - 1])) nodes.push('last'); - // "'virtual' elements" - nodes.unshift('before'); // "0 is a valid index" - nodes.push('after'); // "n+2 is a valid index" - return nodes -}; - -const getNodeByIndex = (node, index) => node ? indexChildNodes(node)[index] : null; - -const partsToNode = (node, parts) => { - const { id } = parts[parts.length - 1]; - if (id) { - const el = node.ownerDocument.getElementById(id); - if (el) return { node: el, offset: 0 } - } - for (const { index } of parts) { - const newNode = getNodeByIndex(node, index); - // handle non-existent nodes - if (newNode === 'first') return { node: node.firstChild ?? node } - if (newNode === 'last') return { node: node.lastChild ?? node } - if (newNode === 'before') return { node, before: true } - if (newNode === 'after') return { node, after: true } - node = newNode; - } - const { offset } = parts[parts.length - 1]; - if (!Array.isArray(node)) return { node, offset } - // get underlying text node and offset from the chunk - let sum = 0; - for (const n of node) { - const { length } = n.nodeValue; - if (sum + length >= offset) return { node: n, offset: offset - sum } - sum += length; - } -}; - -const nodeToParts = (node, offset) => { - const { parentNode, id } = node; - const indexed = indexChildNodes(parentNode); - const index = indexed.findIndex(x => - Array.isArray(x) ? x.some(x => x === node) : x === node); - // adjust offset as if merging the text nodes in the chunk - const chunk = indexed[index]; - if (Array.isArray(chunk)) { - let sum = 0; - for (const x of chunk) { - if (x === node) { - sum += offset; - break - } else sum += x.nodeValue.length; - } - offset = sum; - } - const part = { id, index, offset }; - return parentNode !== node.ownerDocument.documentElement - ? nodeToParts(parentNode).concat(part) : [part] -}; - -const toRange = (doc, parts) => { - const startParts = collapse(parts); - const endParts = collapse(parts, true); - - const root = doc.documentElement; - const start = partsToNode(root, startParts[0]); - const end = partsToNode(root, endParts[0]); - - const range = doc.createRange(); - - if (start.before) range.setStartBefore(start.node); - else if (start.after) range.setStartAfter(start.node); - else range.setStart(start.node, start.offset); - - if (end.before) range.setEndBefore(end.node); - else if (end.after) range.setEndAfter(end.node); - else range.setEnd(end.node, end.offset); - return range -}; - -// faster way of getting CFIs for sorted elements in a single parent -const fromElements = elements => { - const results = []; - const { parentNode } = elements[0]; - const parts = nodeToParts(parentNode); - for (const [index, node] of indexChildNodes(parentNode).entries()) { - const el = elements[results.length]; - if (node === el) - results.push(toString([parts.concat({ id: el.id, index })])); - } - return results -}; - -const toElement = (doc, parts) => - partsToNode(doc.documentElement, collapse(parts)).node; - -const NS$1 = { - CONTAINER: "urn:oasis:names:tc:opendocument:xmlns:container", - XHTML: "http://www.w3.org/1999/xhtml", - OPF: "http://www.idpf.org/2007/opf", - EPUB: "http://www.idpf.org/2007/ops", - DC: "http://purl.org/dc/elements/1.1/", - DCTERMS: "http://purl.org/dc/terms/", - ENC: "http://www.w3.org/2001/04/xmlenc#", - NCX: "http://www.daisy.org/z3986/2005/ncx/", - XLINK: "http://www.w3.org/1999/xlink", - SMIL: "http://www.w3.org/ns/SMIL", -}; - -const MIME$2 = { - XML: "application/xml", - NCX: "application/x-dtbncx+xml", - XHTML: "application/xhtml+xml", - HTML: "text/html", - CSS: "text/css", - SVG: "image/svg+xml", - JS: /\/(x-)?(javascript|ecmascript)/, -}; - -// convert to camel case -const camel = (x) => - x.toLowerCase().replace(/[-:](.)/g, (_, g) => g.toUpperCase()); - -// remove leading, trailing, and excess internal whitespace -const whitespacePreLine = (str) => - str ? str.trim().replace(/\s{2,}/g, " ") : ""; - -const filterAttribute = (attr, value, isList) => - isList - ? (el) => el.getAttribute(attr)?.split(/\s/)?.includes(value) - : typeof value === "function" - ? (el) => value(el.getAttribute(attr)) - : (el) => el.getAttribute(attr) === value; - -const getAttributes = - (...xs) => - (el) => - el - ? Object.fromEntries(xs.map((x) => [camel(x), el.getAttribute(x)])) - : null; - -const getElementText$1 = (el) => whitespacePreLine(el?.textContent); - -const childGetter = (doc, ns) => { - // ignore the namespace if it doesn't appear in document at all - const useNS = doc.lookupNamespaceURI(null) === ns || doc.lookupPrefix(ns); - const f = useNS - ? (el, name) => (el) => el.namespaceURI === ns && el.localName === name - : (el, name) => (el) => el.localName === name; - return { - $: (el, name) => [...el.children].find(f(el, name)), - $$: (el, name) => [...el.children].filter(f(el, name)), - $$$: useNS - ? (el, name) => [...el.getElementsByTagNameNS(ns, name)] - : (el, name) => [...el.getElementsByTagName(ns, name)], - }; -}; - -const resolveURL = (url, relativeTo) => { - try { - // if (relativeTo.includes(":")) return new URL(url, relativeTo); - // the base needs to be a valid URL, so set a base URL and then remove it - const root = "whatever://whatever/"; - return decodeURI(new URL(url, root + relativeTo).href.replace(root, "")); - } catch (e) { - console.warn(e); - return url; - } -}; - -const isExternal = (uri) => /^(?!blob)\w+:/i.test(uri); - -// like `path.relative()` in Node.js -const pathRelative = (from, to) => { - if (!from) return to; - const as = from.replace(/\/$/, "").split("/"); - const bs = to.replace(/\/$/, "").split("/"); - const i = (as.length > bs.length ? as : bs).findIndex( - (_, i) => as[i] !== bs[i] - ); - return i < 0 - ? "" - : Array(as.length - i) - .fill("..") - .concat(bs.slice(i)) - .join("/"); -}; - -const pathDirname = (str) => str.slice(0, str.lastIndexOf("/") + 1); - -// replace asynchronously and sequentially -// same techinque as https://stackoverflow.com/a/48032528 -const replaceSeries$1 = async (str, regex, f) => { - const matches = []; - str.replace(regex, (...args) => (matches.push(args), null)); - const results = []; - for (const args of matches) results.push(await f(...args)); - return str.replace(regex, () => results.shift()); -}; - -const regexEscape = (str) => str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); - -const LANGS = { attrs: ["dir", "xml:lang"] }; -const ALTS = { - name: "alternate-script", - many: true, - ...LANGS, - props: ["file-as"], -}; -const CONTRIB = { - many: true, - ...LANGS, - props: [{ name: "role", many: true, attrs: ["scheme"] }, "file-as", ALTS], -}; -const METADATA = [ - { - name: "title", - many: true, - ...LANGS, - props: ["title-type", "display-seq", "file-as", ALTS], - }, - { - name: "identifier", - many: true, - props: [{ name: "identifier-type", attrs: ["scheme"] }], - }, - { name: "language", many: true }, - { name: "creator", ...CONTRIB }, - { name: "contributor", ...CONTRIB }, - { name: "publisher", ...LANGS, props: ["file-as", ALTS] }, - { name: "description", ...LANGS, props: [ALTS] }, - { name: "rights", ...LANGS, props: [ALTS] }, - { name: "date" }, - { name: "dcterms:modified", type: "meta" }, - { name: "subject", many: true, ...LANGS, props: ["term", "authority", ALTS] }, - { - name: "belongs-to-collection", - type: "meta", - many: true, - ...LANGS, - props: [ - "collection-type", - "group-position", - "dcterms:identifier", - "file-as", - ALTS, - { name: "belongs-to-collection", recursive: true }, - ], - }, -]; - -// NOTE: this only gets properties defined with the `refines` attribute, -// which is used in EPUB 3.0, deprecated in 3.1, then restored in 3.2; -// no support for `opf:` attributes of 2.0 and 3.1 -const getMetadata = (opf) => { - const { $, $$ } = childGetter(opf, NS$1.OPF); - - const $metadata = $(opf.documentElement, "metadata"); - const els = Array.from($metadata.children); - const getValue = (obj, el) => { - if (!el) return null; - const { props = [], attrs = [] } = obj; - const value = getElementText$1(el); - if (!props.length && !attrs.length) return value; - const id = el.getAttribute("id"); - const refines = id ? els.filter(filterAttribute("refines", "#" + id)) : []; - return Object.fromEntries( - [["value", value]] - .concat( - props.map((prop) => { - const { many, recursive } = prop; - const name = typeof prop === "string" ? prop : prop.name; - const filter = filterAttribute("property", name); - const subobj = recursive ? obj : prop; - return [ - camel(name), - many - ? refines.filter(filter).map((el) => getValue(subobj, el)) - : getValue(subobj, refines.find(filter)), - ]; - }) - ) - .concat(attrs.map((attr) => [camel(attr), el.getAttribute(attr)])) - ); - }; - const arr = els.filter(filterAttribute("refines", null)); - const metadata = Object.fromEntries( - METADATA.map((obj) => { - const { type, name, many } = obj; - const filter = - type === "meta" - ? (el) => - el.namespaceURI === NS$1.OPF && el.getAttribute("property") === name - : (el) => el.namespaceURI === NS$1.DC && el.localName === name; - return [ - camel(name), - many - ? arr.filter(filter).map((el) => getValue(obj, el)) - : getValue(obj, arr.find(filter)), - ]; - }) - ); - - const getProperties = (prefix) => - Object.fromEntries( - $$($metadata, "meta") - .filter(filterAttribute("property", (x) => x?.startsWith(prefix))) - .map((el) => [ - el.getAttribute("property").replace(prefix, ""), - getElementText$1(el), - ]) - ); - const rendition = getProperties("rendition:"); - const media = getProperties("media:"); - return { metadata, rendition, media }; -}; - -const parseNav = (doc, resolve = (f) => f) => { - const { $, $$, $$$ } = childGetter(doc, NS$1.XHTML); - const resolveHref = (href) => (href ? decodeURI(resolve(href)) : null); - const parseLI = (getType) => ($li) => { - const $a = $($li, "a") ?? $($li, "span"); - const $ol = $($li, "ol"); - const href = resolveHref($a?.getAttribute("href")); - const label = getElementText$1($a) || $a?.getAttribute("title"); - // TODO: get and concat alt/title texts in content - const result = { label, href, subitems: parseOL($ol) }; - if (getType) result.type = $a?.getAttributeNS(NS$1.EPUB, "type")?.split(/\s/); - return result; - }; - const parseOL = ($ol, getType) => - $ol ? $$($ol, "li").map(parseLI(getType)) : null; - const parseNav = ($nav, getType) => parseOL($($nav, "ol"), getType); - - const $$nav = $$$(doc, "nav"); - let toc = null, - pageList = null, - landmarks = null, - others = []; - for (const $nav of $$nav) { - const type = $nav.getAttributeNS(NS$1.EPUB, "type")?.split(/\s/) ?? []; - if (type.includes("toc")) toc ??= parseNav($nav); - else if (type.includes("page-list")) pageList ??= parseNav($nav); - else if (type.includes("landmarks")) landmarks ??= parseNav($nav, true); - else - others.push({ - label: getElementText$1($nav.firstElementChild), - type, - list: parseNav($nav), - }); - } - return { toc, pageList, landmarks, others }; -}; - -const parseNCX = (doc, resolve = (f) => f) => { - const { $, $$ } = childGetter(doc, NS$1.NCX); - const resolveHref = (href) => (href ? decodeURI(resolve(href)) : null); - const parseItem = (el) => { - const $label = $(el, "navLabel"); - const $content = $(el, "content"); - const label = getElementText$1($label); - const href = resolveHref($content.getAttribute("src")); - if (el.localName === "navPoint") { - const els = $$(el, "navPoint"); - return { label, href, subitems: els.length ? els.map(parseItem) : null }; - } - return { label, href }; - }; - const parseList = (el, itemName) => $$(el, itemName).map(parseItem); - const getSingle = (container, itemName) => { - const $container = $(doc.documentElement, container); - return $container ? parseList($container, itemName) : null; - }; - return { - toc: getSingle("navMap", "navPoint"), - pageList: getSingle("pageList", "pageTarget"), - others: $$(doc.documentElement, "navList").map((el) => ({ - label: getElementText$1($(el, "navLabel")), - list: parseList(el, "navTarget"), - })), - }; -}; - -const parseClock = (str) => { - if (!str) return; - const parts = str.split(":").map((x) => parseFloat(x)); - if (parts.length === 3) { - const [h, m, s] = parts; - return h * 60 * 60 + m * 60 + s; - } - if (parts.length === 2) { - const [m, s] = parts; - return m * 60 + s; - } - const [x, unit] = str.split(/(?=[^\d.])/); - const n = parseFloat(x); - const f = - unit === "h" ? 60 * 60 : unit === "min" ? 60 : unit === "ms" ? 0.001 : 1; - return n * f; -}; - -const parseSMIL = (doc, resolve = (f) => f) => { - const { $, $$$ } = childGetter(doc, NS$1.SMIL); - const resolveHref = (href) => (href ? decodeURI(resolve(href)) : null); - return $$$(doc, "par").map(($par) => { - const id = $($par, "text")?.getAttribute("src")?.split("#")?.[1]; - const $audio = $($par, "audio"); - return $audio - ? { - id, - audio: { - src: resolveHref($audio.getAttribute("src")), - clipBegin: parseClock($audio.getAttribute("clipBegin")), - clipEnd: parseClock($audio.getAttribute("clipEnd")), - }, - } - : { id }; - }); -}; - -const isUUID = - /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})/; - -const getUUID = (opf) => { - for (const el of opf.getElementsByTagNameNS(NS$1.DC, "identifier")) { - const [id] = getElementText$1(el).split(":").slice(-1); - if (isUUID.test(id)) return id; - } - return ""; -}; - -const getIdentifier = (opf) => - getElementText$1( - opf.getElementById(opf.documentElement.getAttribute("unique-identifier")) ?? - opf.getElementsByTagNameNS(NS$1.DC, "identifier")[0] - ); - -// https://www.w3.org/publishing/epub32/epub-ocf.html#sec-resource-obfuscation -const deobfuscate = async (key, length, blob) => { - const array = new Uint8Array(await blob.slice(0, length).arrayBuffer()); - length = Math.min(length, array.length); - for (var i = 0; i < length; i++) array[i] = array[i] ^ key[i % key.length]; - return new Blob([array, blob.slice(length)], { type: blob.type }); -}; - -const WebCryptoSHA1 = async (str) => { - const data = new TextEncoder().encode(str); - const buffer = await globalThis.crypto.subtle.digest("SHA-1", data); - return new Uint8Array(buffer); -}; - -const deobfuscators = (sha1 = WebCryptoSHA1) => ({ - "http://www.idpf.org/2008/embedding": { - key: (opf) => - sha1( - getIdentifier(opf) - // eslint-disable-next-line no-control-regex - .replaceAll(/[\u0020\u0009\u000d\u000a]/g, "") - ), - decode: (key, blob) => deobfuscate(key, 1040, blob), - }, - "http://ns.adobe.com/pdf/enc#RC": { - key: (opf) => { - const uuid = getUUID(opf).replaceAll("-", ""); - return Uint8Array.from({ length: 16 }, (_, i) => - parseInt(uuid.slice(i * 2, i * 2 + 2), 16) - ); - }, - decode: (key, blob) => deobfuscate(key, 1024, blob), - }, -}); - -class Encryption { - #uris = new Map(); - #decoders = new Map(); - #algorithms; - constructor(algorithms) { - this.#algorithms = algorithms; - } - async init(encryption, opf) { - if (!encryption) return; - const data = Array.from( - encryption.getElementsByTagNameNS(NS$1.ENC, "EncryptedData"), - (el) => ({ - algorithm: el - .getElementsByTagNameNS(NS$1.ENC, "EncryptionMethod")[0] - ?.getAttribute("Algorithm"), - uri: el - .getElementsByTagNameNS(NS$1.ENC, "CipherReference")[0] - ?.getAttribute("URI"), - }) - ); - for (const { algorithm, uri } of data) { - if (!this.#decoders.has(algorithm)) { - const algo = this.#algorithms[algorithm]; - if (!algo) { - console.warn("Unknown encryption algorithm"); - continue; - } - const key = await algo.key(opf); - this.#decoders.set(algorithm, (blob) => algo.decode(key, blob)); - } - this.#uris.set(uri, algorithm); - } - } - getDecoder(uri) { - return this.#decoders.get(this.#uris.get(uri)) ?? ((x) => x); - } -} - -class Resources { - constructor({ opf, resolveHref }) { - this.opf = opf; - const { $, $$, $$$ } = childGetter(opf, NS$1.OPF); - const $manifest = $(opf.documentElement, "manifest"); - const $spine = $(opf.documentElement, "spine"); - const $$itemref = $$($spine, "itemref"); - - this.manifest = $$($manifest, "item") - .map( - getAttributes("href", "id", "media-type", "properties", "media-overlay") - ) - .map((item) => { - item.href = resolveHref(item.href); - item.properties = item.properties?.split(/\s/); - return item; - }); - //适配部分不标准的epub - if (this.manifest.length === 0) { - this.manifest = Array.from($manifest.children).map(item => { - const attrs = getAttributes("href", "id", "media-type", "properties", "media-overlay")(item); - attrs.href = resolveHref(attrs.href); - attrs.properties = attrs.properties?.split(/\s/); - return attrs; - }); - } - this.spine = $$itemref - .map(getAttributes("idref", "id", "linear", "properties")) - .map((item) => ((item.properties = item.properties?.split(/\s/)), item)); - this.pageProgressionDirection = $spine.getAttribute( - "page-progression-direction" - ); - - this.navPath = this.getItemByProperty("nav")?.href; - this.ncxPath = ( - this.getItemByID($spine.getAttribute("toc")) ?? - this.manifest.find((item) => item.mediaType === MIME$2.NCX) - )?.href; - - const $guide = $(opf.documentElement, "guide"); - if ($guide) - this.guide = $$($guide, "reference") - .map(getAttributes("type", "title", "href")) - .map(({ type, title, href }) => ({ - label: title, - type: type.split(/\s/), - href: resolveHref(href), - })); - this.cover = - this.getItemByProperty("cover-image") ?? - this.getItemByID("cover-image") ?? - // EPUB 2 compat - this.getItemByID( - $$$(opf, "meta") - .find(filterAttribute("name", "cover")) - ?.getAttribute("content") - ) ?? - this.getItemByID("cover") ?? - this.getItemByID("cover.jpg") ?? - this.getItemByID("cover.png") ?? - this.getItemByID("cover.jpeg") ?? - this.getItemByHref( - this.guide?.find((ref) => ref.type.includes("cover") && !ref.href.includes("html") && !ref.href.includes("xml"))?.href - ); - - this.cfis = fromElements($$itemref); - } - getItemByID(id) { - return this.manifest.find((item) => item.id === id); - } - getItemByHref(href) { - return this.manifest.find((item) => item.href === href); - } - getItemByProperty(prop) { - return this.manifest.find((item) => item.properties?.includes(prop)); - } - resolveCFI(cfi) { - const parts = parse(cfi); - const top = (parts.parent ?? parts).shift(); - let $itemref = toElement(this.opf, top); - // make sure it's an idref; if not, try again without the ID assertion - // mainly because Epub.js used to generate wrong ID assertions - // https://github.com/futurepress/epub.js/issues/1236 - if ($itemref && $itemref.nodeName !== "idref") { - top.at(-1).id = null; - $itemref = toElement(this.opf, top); - } - const idref = $itemref?.getAttribute("idref"); - const index = this.spine.findIndex((item) => item.idref === idref); - const anchor = (doc) => toRange(doc, parts); - return { index, anchor }; - } -} - -class Loader { - #cache = new Map(); - #children = new Map(); - #refCount = new Map(); - allowScript = false; - constructor({ loadText, loadBlob, resources }) { - this.loadText = loadText; - this.loadBlob = loadBlob; - this.manifest = resources.manifest; - this.assets = resources.manifest; - // needed only when replacing in (X)HTML w/o parsing (see below) - //.filter(({ mediaType }) => ![MIME.XHTML, MIME.HTML].includes(mediaType)) - } - createURL(href, data, type, parent) { - if (!data) return ""; - const url = URL.createObjectURL(new Blob([data], { type })); - this.#cache.set(href, url); - this.#refCount.set(href, 1); - if (parent) { - const childList = this.#children.get(parent); - if (childList) childList.push(href); - else this.#children.set(parent, [href]); - } - return url; - } - ref(href, parent) { - const childList = this.#children.get(parent); - if (!childList?.includes(href)) { - this.#refCount.set(href, this.#refCount.get(href) + 1); - //console.log(`referencing ${href}, now ${this.#refCount.get(href)}`) - if (childList) childList.push(href); - else this.#children.set(parent, [href]); - } - return this.#cache.get(href); - } - unref(href) { - if (!this.#refCount.has(href)) return; - const count = this.#refCount.get(href) - 1; - //console.log(`unreferencing ${href}, now ${count}`) - if (count < 1) { - //console.log(`unloading ${href}`) - URL.revokeObjectURL(this.#cache.get(href)); - this.#cache.delete(href); - this.#refCount.delete(href); - // unref children - const childList = this.#children.get(href); - if (childList) while (childList.length) this.unref(childList.pop()); - this.#children.delete(href); - } else this.#refCount.set(href, count); - } - // load manifest item, recursively loading all resources as needed - async loadItem(item, parents = []) { - if (!item) return null; - const { href, mediaType } = item; - - const isScript = MIME$2.JS.test(item.mediaType); - if (isScript && !this.allowScript) return null; - - const parent = parents.at(-1); - if (this.#cache.has(href)) return this.ref(href, parent); - - const shouldReplace = - (isScript || - [MIME$2.XHTML, MIME$2.HTML, MIME$2.CSS, MIME$2.SVG].includes(mediaType)) && - // prevent circular references - parents.every((p) => p !== href); - if (shouldReplace) return this.loadReplaced(item, parents); - return this.createURL(href, await this.loadBlob(href), mediaType, parent); - } - async loadHref(href, base, parents = []) { - if (isExternal(href)) return href; - const path = resolveURL(href, base); - let item = this.manifest.find((item) => item.href === path); - if (!item) { - item = { href: path, mediaType: "" }; - } - return this.loadItem(item, parents.concat(base)); - } - async loadReplaced(item, parents = []) { - const { href, mediaType } = item; - const parent = parents.at(-1); - const str = await this.loadText(href); - if (!str) return null; - - // note that one can also just use `replaceString` for everything: - // ``` - // const replaced = await this.replaceString(str, href, parents) - // return this.createURL(href, replaced, mediaType, parent) - // ``` - // which is basically what Epub.js does, which is simpler, but will - // break things like iframes (because you don't want to replace links) - // or text that just happen to be paths - - // parse and replace in HTML - if ([MIME$2.XHTML, MIME$2.HTML, MIME$2.SVG].includes(mediaType)) { - let doc = new DOMParser().parseFromString(str.trim(), mediaType); - // change to HTML if it's not valid XHTML - if (mediaType === MIME$2.XHTML && doc.querySelector("parsererror")) { - console.warn(doc.querySelector("parsererror").innerText); - item.mediaType = MIME$2.HTML; - doc = new DOMParser().parseFromString(str.trim(), item.mediaType); - } - // replace hrefs in XML processing instructions - // this is mainly for SVGs that use xml-stylesheet - if ([MIME$2.XHTML, MIME$2.SVG].includes(item.mediaType)) { - let child = doc.firstChild; - while (child instanceof ProcessingInstruction) { - if (child.data) { - const replacedData = await replaceSeries$1( - child.data, - /(?:^|\s*)(href\s*=\s*['"])([^'"]*)(['"])/i, - (_, p1, p2, p3) => - this.loadHref(p2, href, parents).then((p2) => `${p1}${p2}${p3}`) - ); - child.replaceWith( - doc.createProcessingInstruction(child.target, replacedData) - ); - } - child = child.nextSibling; - } - } - // replace hrefs (excluding anchors) - // TODO: srcset? - const replace = async (el, attr) => - el.setAttribute( - attr, - await this.loadHref(el.getAttribute(attr), href, parents) - ); - for (const el of doc.querySelectorAll("link[href]")) - await replace(el, "href"); - for (const el of doc.querySelectorAll("[src]")) await replace(el, "src"); - for (const el of doc.querySelectorAll("[poster]")) - await replace(el, "poster"); - for (const el of doc.querySelectorAll("object[data]")) - await replace(el, "data"); - for (const el of doc.querySelectorAll("[*|href]:not([href]")) - el.setAttributeNS( - NS$1.XLINK, - "href", - await this.loadHref( - el.getAttributeNS(NS$1.XLINK, "href"), - href, - parents - ) - ); - // replace inline styles - for (const el of doc.querySelectorAll("style")) - if (el.textContent) - el.textContent = await this.replaceCSS(el.textContent, href, parents); - for (const el of doc.querySelectorAll("[style]")) - el.setAttribute( - "style", - await this.replaceCSS(el.getAttribute("style"), href, parents) - ); - // TODO: replace inline scripts? probably not worth the trouble - const result = new XMLSerializer().serializeToString(doc); - return this.createURL(href, result, item.mediaType, parent); - } - - const result = - mediaType === MIME$2.CSS - ? await this.replaceCSS(str, href, parents) - : await this.replaceString(str, href, parents); - return this.createURL(href, result, mediaType, parent); - } - async replaceCSS(str, href, parents = []) { - const replacedUrls = await replaceSeries$1( - str, - /url\(\s*["']?([^'"\n]*?)\s*["']?\s*\)/gi, - (_, url) => - this.loadHref(url, href, parents).then((url) => `url("${url}")`) - ); - // apart from `url()`, strings can be used for `@import` (but why?!) - const replacedImports = await replaceSeries$1( - replacedUrls, - /@import\s*["']([^"'\n]*?)["']/gi, - (_, url) => - this.loadHref(url, href, parents).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(/-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)/gi, - (_, x) => `-webkit-column-break-${x}` - ) - ); - } - // find & replace all possible relative paths for all assets without parsing - replaceString(str, href, parents = []) { - const assetMap = new Map(); - const urls = this.assets - .map((asset) => { - // do not replace references to the file itself - if (asset.href === href) return; - // href was decoded and resolved when parsing the manifest - const relative = pathRelative(pathDirname(href), asset.href); - const relativeEnc = encodeURI(relative); - const rootRelative = "/" + asset.href; - const rootRelativeEnc = encodeURI(rootRelative); - const set = new Set([ - relative, - relativeEnc, - rootRelative, - rootRelativeEnc, - ]); - for (const url of set) assetMap.set(url, asset); - return Array.from(set); - }) - .flat() - .filter((x) => x); - if (!urls.length) return str; - const regex = new RegExp(urls.map(regexEscape).join("|"), "g"); - return replaceSeries$1(str, regex, async (match) => - this.loadItem( - assetMap.get(match.replace(/^\//, "")), - parents.concat(href) - ) - ); - } - unloadItem(item) { - this.unref(item?.href); - } -} - -const getHTMLFragment = (doc, id) => - doc.getElementById(id) ?? doc.querySelector(`[name="${CSS.escape(id)}"]`); - -const getPageSpread$1 = (properties) => { - for (const p of properties) { - if (p === "page-spread-left" || p === "rendition:page-spread-left") - return "left"; - if (p === "page-spread-right" || p === "rendition:page-spread-right") - return "right"; - if (p === "rendition:page-spread-center") return "center"; - } -}; - -class EPUB { - parser = new DOMParser(); - #encryption; - constructor({ loadText, loadBlob, getSize, sha1 }) { - this.loadText = loadText; - this.loadBlob = loadBlob; - this.getSize = getSize; - this.#encryption = new Encryption(deobfuscators(sha1)); - } - #parseXML(str) { - if (str && str.includes("opf:scheme")) { - str = str.replaceAll("opf:scheme", "scheme"); - } - //error on line 41 at column 46: Comment must not contain '--' (double-hyphen) - if (str) { - // 修复常见的 XML 问题 - str = str - // 移除 BOM - .replace(/^\uFEFF/, '') - // 修复注释中的双连字符 - .replace(//g, (match, content) => { - const fixedContent = content.replace(/--/g, '- -'); - return ``; - }) - // 修复未正确转义的 & 符号(除了实体引用) - .replace(/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);)/g, '&') - // 移除控制字符(保留换行符、回车符、制表符) - .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - } - - - return str ? this.parser.parseFromString(str.trim(), MIME$2.XML) : null; - } - async #loadXML(uri) { - return this.#parseXML(await this.loadText(uri)); - } - async init() { - const $container = await this.#loadXML("META-INF/container.xml"); - if (!$container) throw new Error("Failed to load container file"); - - const opfs = Array.from( - $container.getElementsByTagNameNS(NS$1.CONTAINER, "rootfile"), - getAttributes("full-path", "media-type") - ).filter((file) => file.mediaType === "application/oebps-package+xml"); - - if (!opfs.length) - throw new Error("No package document defined in container"); - const opfPath = opfs[0].fullPath; - const opf = await this.#loadXML(opfPath); - if (!opf) throw new Error("Failed to load package document"); - //当opf中包含parsererror时,说明不是标准的xml格式 - if (opf.querySelector("parsererror")) { - throw new Error("Package document is not a valid XML"); - } - - const $encryption = await this.#loadXML("META-INF/encryption.xml"); - await this.#encryption.init($encryption, opf); - - this.resources = new Resources({ - opf, - resolveHref: (url) => resolveURL(url, opfPath), - }); - const loader = new Loader({ - loadText: this.loadText, - loadBlob: (uri) => - Promise.resolve(this.loadBlob(uri)).then( - this.#encryption.getDecoder(uri) - ), - resources: this.resources, - }); - this.sections = this.resources.spine - .map((spineItem, index) => { - const { idref, linear, properties = [] } = spineItem; - const item = this.resources.getItemByID(idref); - if (!item) { - console.warn(`Could not find item with ID "${idref}" in manifest`); - return null; - } - return { - id: this.resources.getItemByID(idref)?.href, - load: () => loader.loadItem(item), - unload: () => loader.unloadItem(item), - createDocument: () => this.loadDocument(item), - size: this.getSize(item.href), - cfi: this.resources.cfis[index], - linear, - pageSpread: getPageSpread$1(properties), - resolveHref: (href) => resolveURL(href, item.href), - loadMediaOverlay: () => this.loadMediaOverlay(item), - }; - }); - - const { navPath, ncxPath } = this.resources; - if (navPath) - try { - const resolve = (url) => resolveURL(url, navPath); - const nav = parseNav(await this.#loadXML(navPath), resolve); - this.toc = nav.toc; - this.pageList = nav.pageList; - this.landmarks = nav.landmarks; - } catch (e) { - console.warn(e); - } - if ((!this.toc || this.toc.length === 0) && ncxPath) - try { - const resolve = (url) => resolveURL(url, ncxPath); - const ncx = parseNCX(await this.#loadXML(ncxPath), resolve); - this.toc = ncx.toc; - this.pageList = ncx.pageList; - } catch (e) { - console.warn(e); - } - this.landmarks ??= this.resources.guide; - - const { metadata, rendition, media } = getMetadata(opf); - this.rendition = rendition; - this.media = media; - media.duration = parseClock(media.duration); - this.dir = this.resources.pageProgressionDirection; - - this.rawMetadata = metadata; // useful for debugging, i guess - const title = metadata?.title?.[0]; - this.metadata = { - title: title?.value, - sortAs: title?.fileAs, - language: metadata?.language, - identifier: getIdentifier(opf), - description: metadata?.description?.value, - publisher: metadata?.publisher?.value, - published: metadata?.date, - modified: metadata?.dctermsModified, - subject: metadata?.subject - ?.filter(({ value, code }) => value || code) - ?.map(({ value, code, scheme }) => ({ name: value, code, scheme })), - rights: metadata?.rights?.value, - }; - const relators = { - art: "artist", - aut: "author", - bkp: "producer", - clr: "colorist", - edt: "editor", - ill: "illustrator", - trl: "translator", - pbl: "publisher", - }; - const mapContributor = (defaultKey) => (obj) => { - const keys = [ - ...new Set( - obj.role?.map( - ({ value, scheme }) => - (!scheme || scheme === "marc:relators" - ? relators[value] - : null) ?? defaultKey - ) - ), - ]; - const value = { name: obj.value, sortAs: obj.fileAs }; - return [keys?.length ? keys : [defaultKey], value]; - }; - metadata?.creator - ?.map(mapContributor("author")) - ?.concat(metadata?.contributor?.map?.(mapContributor("contributor"))) - ?.forEach(([keys, value]) => - keys.forEach((key) => { - if (this.metadata[key]) this.metadata[key].push(value); - else this.metadata[key] = [value]; - }) - ); - - return this; - } - async loadDocument(item) { - const str = await this.loadText(item.href); - return this.parser.parseFromString(str.trim(), item.mediaType); - } - async loadMediaOverlay(item) { - const id = item.mediaOverlay; - if (!id) return null; - const media = this.resources.getItemByID(id); - const doc = await this.#loadXML(media.href); - const parsed = parseSMIL(doc, (url) => resolveURL(url, media.href)); - return parsed; - } - resolveCFI(cfi) { - return this.resources.resolveCFI(cfi); - } - resolveHref(href) { - const [path, hash] = href.split("#"); - const item = this.resources.getItemByHref(decodeURI(path)); - if (!item) return null; - const index = this.resources.spine.findIndex( - ({ idref }) => idref === item.id - ); - const anchor = hash ? (doc) => getHTMLFragment(doc, hash) : () => 0; - return { index, anchor }; - } - splitTOCHref(href) { - return href?.split("#") ?? []; - } - getTOCFragment(doc, id) { - return ( - doc.getElementById(id) ?? doc.querySelector(`[name="${CSS.escape(id)}"]`) - ); - } - isExternal(uri) { - return isExternal(uri); - } - async getCover() { - const cover = this.resources?.cover; - return cover?.href - ? new Blob([await this.loadBlob(cover.href)], { type: cover.mediaType }) - : null; - } - async getCalibreBookmarks() { - const txt = await this.loadText("META-INF/calibre_bookmarks.txt"); - const magic = "encoding=json+base64:"; - if (txt?.startsWith(magic)) { - const json = atob(txt.slice(magic.length)); - return JSON.parse(json); - } - } -} - -const s2tData = { - 㐷: "傌", - 㐹: "㑶", - 㐽: "偑", - 㑇: "㑳", - 㑈: "倲", - 㑔: "㑯", - 㑩: "儸", - 㓆: "𠗣", - 㓥: "劏", - 㓰: "劃", - 㔉: "劚", - 㖊: "噚", - 㖞: "喎", - 㘎: "㘚", - 㚯: "㜄", - 㛀: "媰", - 㛟: "𡞵", - 㛠: "𡢃", - 㛣: "㜏", - 㛤: "孋", - 㛿: "𡠹", - 㟆: "㠏", - 㟜: "𡾱", - 㟥: "嵾", - 㡎: "幓", - 㤘: "㥮", - 㤽: "懤", - 㥪: "慺", - 㧏: "掆", - 㧐: "㩳", - 㧑: "撝", - 㧟: "擓", - 㧰: "擽", - 㨫: "㩜", - 㭎: "棡", - 㭏: "椲", - 㭣: "𣙎", - 㭤: "樢", - 㭴: "樫", - 㱩: "殰", - 㱮: "殨", - 㲿: "瀇", - 㳔: "濧", - 㳕: "灡", - 㳠: "澾", - 㳡: "濄", - 㳢: "𣾷", - 㳽: "瀰", - 㴋: "潚", - 㶉: "鸂", - 㶶: "燶", - 㶽: "煱", - 㺍: "獱", - 㻅: "璯", - 㻏: "𤫩", - 㻘: "𤪺", - 䀥: "䁻", - 䁖: "瞜", - 䂵: "碽", - 䃅: "磾", - 䅉: "稏", - 䅟: "穇", - 䅪: "𥢢", - 䇲: "筴", - 䉤: "籔", - 䌶: "䊷", - 䌷: "紬", - 䌸: "縳", - 䌹: "絅", - 䌺: "䋙", - 䌻: "䋚", - 䌼: "綐", - 䌽: "綵", - 䌾: "䋻", - 䌿: "䋹", - 䍀: "繿", - 䍁: "繸", - 䍠: "䍦", - 䎬: "䎱", - 䏝: "膞", - 䑽: "𦪙", - 䓓: "薵", - 䓕: "薳", - 䓖: "藭", - 䓨: "罃", - 䗖: "螮", - 䘛: "𧝞", - 䘞: "𧜗", - 䙊: "𧜵", - 䙌: "䙡", - 䙓: "襬", - 䜣: "訢", - 䜤: "鿁", - 䜥: "𧩙", - 䜧: "䜀", - 䜩: "讌", - 䝙: "貙", - 䞌: "𧵳", - 䞍: "䝼", - 䞎: "𧶧", - 䞐: "賰", - 䟢: "躎", - 䢀: "𨊰", - 䢁: "𨊸", - 䢂: "𨋢", - 䥺: "釾", - 䥽: "鏺", - 䥾: "䥱", - 䥿: "𨯅", - 䦀: "𨦫", - 䦁: "𨧜", - 䦂: "䥇", - 䦃: "鐯", - 䦅: "鐥", - 䦆: "钁", - 䦶: "䦛", - 䦷: "䦟", - 䩄: "靦", - 䭪: "𩞯", - 䯃: "𩣑", - 䯄: "騧", - 䯅: "䯀", - 䲝: "䱽", - 䲞: "𩶘", - 䲟: "鮣", - 䲠: "鰆", - 䲡: "鰌", - 䲢: "鰧", - 䲣: "䱷", - 䴓: "鳾", - 䴔: "鵁", - 䴕: "鴷", - 䴖: "鶄", - 䴗: "鶪", - 䴘: "鷉", - 䴙: "鸊", - 䶮: "龑", - 万: "萬", - 与: "與", - 丑: "醜", - 专: "專", - 业: "業", - 丛: "叢", - 东: "東", - 丝: "絲", - 丢: "丟", - 两: "兩", - 严: "嚴", - 丧: "喪", - 个: "個", - 丰: "豐", - 临: "臨", - 为: "爲", - 丽: "麗", - 举: "舉", - 么: "麼", - 义: "義", - 乌: "烏", - 乐: "樂", - 乔: "喬", - 习: "習", - 乡: "鄉", - 书: "書", - 买: "買", - 乱: "亂", - 了: "了", - 争: "爭", - 于: "於", - 亏: "虧", - 云: "雲", - 亘: "亙", - 亚: "亞", - 产: "產", - 亩: "畝", - 亲: "親", - 亵: "褻", - 亸: "嚲", - 亿: "億", - 仅: "僅", - 仆: "僕", - 仇: "仇", - 从: "從", - 仑: "侖", - 仓: "倉", - 仪: "儀", - 们: "們", - 价: "價", - 仿: "仿", - 众: "衆", - 优: "優", - 伙: "夥", - 会: "會", - 伛: "傴", - 伞: "傘", - 伟: "偉", - 传: "傳", - 伡: "俥", - 伣: "俔", - 伤: "傷", - 伥: "倀", - 伦: "倫", - 伧: "傖", - 伪: "僞", - 伫: "佇", - 体: "體", - 余: "餘", - 佛: "佛", - 佣: "傭", - 佥: "僉", - 侠: "俠", - 侣: "侶", - 侥: "僥", - 侦: "偵", - 侧: "側", - 侨: "僑", - 侩: "儈", - 侪: "儕", - 侬: "儂", - 侭: "儘", - 俊: "俊", - 俣: "俁", - 俦: "儔", - 俨: "儼", - 俩: "倆", - 俪: "儷", - 俫: "倈", - 俭: "儉", - 修: "修", - 借: "借", - 债: "債", - 倾: "傾", - 偬: "傯", - 偻: "僂", - 偾: "僨", - 偿: "償", - 傤: "儎", - 傥: "儻", - 傧: "儐", - 储: "儲", - 傩: "儺", - 僵: "僵", - 儿: "兒", - 克: "克", - 兑: "兌", - 兖: "兗", - 党: "黨", - 兰: "蘭", - 关: "關", - 兴: "興", - 兹: "茲", - 养: "養", - 兽: "獸", - 冁: "囅", - 内: "內", - 冈: "岡", - 册: "冊", - 写: "寫", - 军: "軍", - 农: "農", - 冬: "冬", - 冯: "馮", - 冲: "衝", - 决: "決", - 况: "況", - 冻: "凍", - 净: "淨", - 凄: "悽", - 准: "準", - 凉: "涼", - 凌: "凌", - 减: "減", - 凑: "湊", - 凛: "凜", - 几: "幾", - 凤: "鳳", - 凫: "鳧", - 凭: "憑", - 凯: "凱", - 凶: "兇", - 出: "出", - 击: "擊", - 凿: "鑿", - 刍: "芻", - 划: "劃", - 刘: "劉", - 则: "則", - 刚: "剛", - 创: "創", - 删: "刪", - 别: "別", - 刬: "剗", - 刭: "剄", - 刮: "刮", - 制: "制", - 刹: "剎", - 刽: "劊", - 刾: "㓨", - 刿: "劌", - 剀: "剴", - 剂: "劑", - 剐: "剮", - 剑: "劍", - 剥: "剝", - 剧: "劇", - 劝: "勸", - 办: "辦", - 务: "務", - 劢: "勱", - 动: "動", - 励: "勵", - 劲: "勁", - 劳: "勞", - 势: "勢", - 勋: "勳", - 勚: "勩", - 匀: "勻", - 匦: "匭", - 匮: "匱", - 区: "區", - 医: "醫", - 千: "千", - 升: "升", - 华: "華", - 协: "協", - 单: "單", - 卖: "賣", - 卜: "卜", - 占: "佔", - 卢: "盧", - 卤: "滷", - 卧: "臥", - 卫: "衛", - 却: "卻", - 卷: "卷", - 卺: "巹", - 厂: "廠", - 厅: "廳", - 历: "歷", - 厉: "厲", - 压: "壓", - 厌: "厭", - 厍: "厙", - 厐: "龎", - 厕: "廁", - 厘: "釐", - 厢: "廂", - 厣: "厴", - 厦: "廈", - 厨: "廚", - 厩: "廄", - 厮: "廝", - 县: "縣", - 叁: "叄", - 参: "參", - 叆: "靉", - 叇: "靆", - 双: "雙", - 发: "發", - 变: "變", - 叙: "敘", - 叠: "疊", - 只: "只", - 台: "臺", - 叶: "葉", - 号: "號", - 叹: "嘆", - 叽: "嘰", - 吁: "籲", - 吃: "喫", - 合: "合", - 吊: "吊", - 同: "同", - 后: "後", - 向: "向", - 吓: "嚇", - 吕: "呂", - 吗: "嗎", - 吨: "噸", - 听: "聽", - 启: "啓", - 吴: "吳", - 呐: "吶", - 呒: "嘸", - 呓: "囈", - 呕: "嘔", - 呖: "嚦", - 呗: "唄", - 员: "員", - 呙: "咼", - 呛: "嗆", - 呜: "嗚", - 周: "周", - 咏: "詠", - 咙: "嚨", - 咛: "嚀", - 咝: "噝", - 咤: "吒", - 咨: "諮", - 咸: "鹹", - 咽: "咽", - 哄: "哄", - 响: "響", - 哑: "啞", - 哒: "噠", - 哓: "嘵", - 哔: "嗶", - 哕: "噦", - 哗: "譁", - 哙: "噲", - 哜: "嚌", - 哝: "噥", - 哟: "喲", - 唇: "脣", - 唛: "嘜", - 唝: "嗊", - 唠: "嘮", - 唡: "啢", - 唢: "嗩", - 唤: "喚", - 啧: "嘖", - 啬: "嗇", - 啭: "囀", - 啮: "齧", - 啯: "嘓", - 啰: "囉", - 啴: "嘽", - 啸: "嘯", - 喂: "喂", - 喷: "噴", - 喽: "嘍", - 喾: "嚳", - 嗫: "囁", - 嗳: "噯", - 嘘: "噓", - 嘤: "嚶", - 嘱: "囑", - 噜: "嚕", - 噪: "噪", - 嚣: "囂", - 回: "回", - 团: "團", - 园: "園", - 困: "困", - 囱: "囪", - 围: "圍", - 囵: "圇", - 国: "國", - 图: "圖", - 圆: "圓", - 圣: "聖", - 圹: "壙", - 场: "場", - 坏: "壞", - 块: "塊", - 坚: "堅", - 坛: "壇", - 坜: "壢", - 坝: "壩", - 坞: "塢", - 坟: "墳", - 坠: "墜", - 垄: "壟", - 垅: "壠", - 垆: "壚", - 垒: "壘", - 垦: "墾", - 垩: "堊", - 垫: "墊", - 垭: "埡", - 垯: "墶", - 垱: "壋", - 垲: "塏", - 垴: "堖", - 埘: "塒", - 埙: "壎", - 埚: "堝", - 堑: "塹", - 堕: "墮", - 塆: "壪", - 墙: "牆", - 壮: "壯", - 声: "聲", - 壳: "殼", - 壶: "壺", - 壸: "壼", - 处: "處", - 备: "備", - 复: "復", - 够: "夠", - 夫: "夫", - 头: "頭", - 夸: "誇", - 夹: "夾", - 夺: "奪", - 奁: "奩", - 奂: "奐", - 奋: "奮", - 奖: "獎", - 奥: "奧", - 奸: "奸", - 妆: "妝", - 妇: "婦", - 妈: "媽", - 妩: "嫵", - 妪: "嫗", - 妫: "嬀", - 姗: "姍", - 姜: "姜", - 姹: "奼", - 娄: "婁", - 娅: "婭", - 娆: "嬈", - 娇: "嬌", - 娈: "孌", - 娘: "娘", - 娱: "娛", - 娲: "媧", - 娴: "嫺", - 婳: "嫿", - 婴: "嬰", - 婵: "嬋", - 婶: "嬸", - 媪: "媼", - 媭: "嬃", - 嫒: "嬡", - 嫔: "嬪", - 嫱: "嬙", - 嬷: "嬤", - 孙: "孫", - 学: "學", - 孪: "孿", - 宁: "寧", - 它: "它", - 宝: "寶", - 实: "實", - 宠: "寵", - 审: "審", - 宪: "憲", - 宫: "宮", - 家: "家", - 宽: "寬", - 宾: "賓", - 寝: "寢", - 对: "對", - 寻: "尋", - 导: "導", - 寿: "壽", - 将: "將", - 尔: "爾", - 尘: "塵", - 尝: "嘗", - 尧: "堯", - 尴: "尷", - 尸: "屍", - 尽: "盡", - 局: "局", - 层: "層", - 屃: "屓", - 屉: "屜", - 届: "屆", - 属: "屬", - 屡: "屢", - 屦: "屨", - 屿: "嶼", - 岁: "歲", - 岂: "豈", - 岖: "嶇", - 岗: "崗", - 岘: "峴", - 岚: "嵐", - 岛: "島", - 岩: "巖", - 岭: "嶺", - 岳: "嶽", - 岽: "崬", - 岿: "巋", - 峃: "嶨", - 峄: "嶧", - 峡: "峽", - 峣: "嶢", - 峤: "嶠", - 峥: "崢", - 峦: "巒", - 峰: "峯", - 崂: "嶗", - 崃: "崍", - 崄: "嶮", - 崭: "嶄", - 嵘: "嶸", - 嵚: "嶔", - 嵝: "嶁", - 巅: "巔", - 巨: "巨", - 巩: "鞏", - 巯: "巰", - 币: "幣", - 布: "布", - 帅: "帥", - 师: "師", - 帏: "幃", - 帐: "帳", - 帘: "簾", - 帜: "幟", - 带: "帶", - 帧: "幀", - 席: "席", - 帮: "幫", - 帱: "幬", - 帻: "幘", - 帼: "幗", - 幂: "冪", - 干: "幹", - 并: "並", - 幸: "幸", - 广: "廣", - 庄: "莊", - 庆: "慶", - 床: "牀", - 庐: "廬", - 庑: "廡", - 库: "庫", - 应: "應", - 庙: "廟", - 庞: "龐", - 废: "廢", - 庵: "庵", - 庼: "廎", - 廪: "廩", - 开: "開", - 异: "異", - 弃: "棄", - 弑: "弒", - 张: "張", - 弥: "彌", - 弦: "弦", - 弪: "弳", - 弯: "彎", - 弹: "彈", - 强: "強", - 归: "歸", - 当: "當", - 录: "錄", - 彟: "彠", - 彦: "彥", - 彨: "彲", - 彩: "彩", - 彻: "徹", - 征: "徵", - 径: "徑", - 徕: "徠", - 御: "御", - 忆: "憶", - 忏: "懺", - 志: "志", - 忧: "憂", - 念: "念", - 忾: "愾", - 怀: "懷", - 态: "態", - 怂: "慫", - 怃: "憮", - 怄: "慪", - 怅: "悵", - 怆: "愴", - 怜: "憐", - 总: "總", - 怼: "懟", - 怿: "懌", - 恋: "戀", - 恒: "恆", - 恤: "恤", - 恳: "懇", - 恶: "惡", - 恸: "慟", - 恹: "懨", - 恺: "愷", - 恻: "惻", - 恼: "惱", - 恽: "惲", - 悦: "悅", - 悫: "愨", - 悬: "懸", - 悭: "慳", - 悮: "悞", - 悯: "憫", - 惊: "驚", - 惧: "懼", - 惨: "慘", - 惩: "懲", - 惫: "憊", - 惬: "愜", - 惭: "慚", - 惮: "憚", - 惯: "慣", - 愈: "愈", - 愠: "慍", - 愤: "憤", - 愦: "憒", - 愿: "願", - 慑: "懾", - 慭: "憖", - 懑: "懣", - 懒: "懶", - 懔: "懍", - 戆: "戇", - 戋: "戔", - 戏: "戲", - 戗: "戧", - 战: "戰", - 戚: "戚", - 戬: "戩", - 戯: "戱", - 户: "戶", - 才: "才", - 扎: "扎", - 扑: "撲", - 托: "託", - 扣: "扣", - 执: "執", - 扩: "擴", - 扪: "捫", - 扫: "掃", - 扬: "揚", - 扰: "擾", - 折: "折", - 抚: "撫", - 抛: "拋", - 抟: "摶", - 抠: "摳", - 抡: "掄", - 抢: "搶", - 护: "護", - 报: "報", - 抵: "抵", - 担: "擔", - 拐: "拐", - 拟: "擬", - 拢: "攏", - 拣: "揀", - 拥: "擁", - 拦: "攔", - 拧: "擰", - 拨: "撥", - 择: "擇", - 挂: "掛", - 挚: "摯", - 挛: "攣", - 挜: "掗", - 挝: "撾", - 挞: "撻", - 挟: "挾", - 挠: "撓", - 挡: "擋", - 挢: "撟", - 挣: "掙", - 挤: "擠", - 挥: "揮", - 挦: "撏", - 挨: "挨", - 挽: "挽", - 捝: "挩", - 捞: "撈", - 损: "損", - 捡: "撿", - 换: "換", - 捣: "搗", - 据: "據", - 掳: "擄", - 掴: "摑", - 掷: "擲", - 掸: "撣", - 掺: "摻", - 掼: "摜", - 揽: "攬", - 揾: "搵", - 揿: "撳", - 搀: "攙", - 搁: "擱", - 搂: "摟", - 搄: "揯", - 搅: "攪", - 搜: "搜", - 携: "攜", - 摄: "攝", - 摅: "攄", - 摆: "擺", - 摇: "搖", - 摈: "擯", - 摊: "攤", - 撄: "攖", - 撑: "撐", - 撵: "攆", - 撷: "擷", - 撸: "擼", - 撺: "攛", - 擜: "㩵", - 擞: "擻", - 攒: "攢", - 敌: "敵", - 敚: "敓", - 敛: "斂", - 敩: "斆", - 数: "數", - 斋: "齋", - 斓: "斕", - 斗: "鬥", - 斩: "斬", - 断: "斷", - 旋: "旋", - 无: "無", - 旧: "舊", - 时: "時", - 旷: "曠", - 旸: "暘", - 昆: "昆", - 昙: "曇", - 昵: "暱", - 昼: "晝", - 昽: "曨", - 显: "顯", - 晋: "晉", - 晒: "曬", - 晓: "曉", - 晔: "曄", - 晕: "暈", - 晖: "暉", - 暂: "暫", - 暅: "𣈶", - 暗: "暗", - 暧: "曖", - 曲: "曲", - 术: "術", - 朱: "朱", - 朴: "樸", - 机: "機", - 杀: "殺", - 杂: "雜", - 权: "權", - 杆: "杆", - 杠: "槓", - 条: "條", - 来: "來", - 杨: "楊", - 杩: "榪", - 杯: "杯", - 杰: "傑", - 松: "松", - 板: "板", - 极: "極", - 构: "構", - 枞: "樅", - 枢: "樞", - 枣: "棗", - 枥: "櫪", - 枧: "梘", - 枨: "棖", - 枪: "槍", - 枫: "楓", - 枭: "梟", - 柜: "櫃", - 柠: "檸", - 柽: "檉", - 栀: "梔", - 栅: "柵", - 标: "標", - 栈: "棧", - 栉: "櫛", - 栊: "櫳", - 栋: "棟", - 栌: "櫨", - 栎: "櫟", - 栏: "欄", - 树: "樹", - 栖: "棲", - 栗: "栗", - 样: "樣", - 核: "核", - 栾: "欒", - 桠: "椏", - 桡: "橈", - 桢: "楨", - 档: "檔", - 桤: "榿", - 桥: "橋", - 桦: "樺", - 桧: "檜", - 桨: "槳", - 桩: "樁", - 桪: "樳", - 梁: "梁", - 梦: "夢", - 梼: "檮", - 梾: "棶", - 梿: "槤", - 检: "檢", - 棁: "梲", - 棂: "欞", - 椁: "槨", - 椝: "槼", - 椟: "櫝", - 椠: "槧", - 椢: "槶", - 椤: "欏", - 椫: "樿", - 椭: "橢", - 椮: "槮", - 楼: "樓", - 榄: "欖", - 榅: "榲", - 榇: "櫬", - 榈: "櫚", - 榉: "櫸", - 榝: "樧", - 槚: "檟", - 槛: "檻", - 槟: "檳", - 槠: "櫧", - 横: "橫", - 樯: "檣", - 樱: "櫻", - 橥: "櫫", - 橱: "櫥", - 橹: "櫓", - 橼: "櫞", - 檩: "檁", - 欢: "歡", - 欤: "歟", - 欧: "歐", - 欲: "欲", - 歼: "殲", - 殁: "歿", - 殇: "殤", - 残: "殘", - 殒: "殞", - 殓: "殮", - 殚: "殫", - 殡: "殯", - 殴: "毆", - 毁: "毀", - 毂: "轂", - 毕: "畢", - 毙: "斃", - 毡: "氈", - 毵: "毿", - 毶: "𣯶", - 氇: "氌", - 气: "氣", - 氢: "氫", - 氩: "氬", - 氲: "氳", - 汇: "匯", - 汉: "漢", - 汤: "湯", - 汹: "洶", - 沄: "澐", - 沈: "沈", - 沟: "溝", - 没: "沒", - 沣: "灃", - 沤: "漚", - 沥: "瀝", - 沦: "淪", - 沧: "滄", - 沨: "渢", - 沩: "潙", - 沪: "滬", - 沾: "沾", - 泛: "泛", - 泞: "濘", - 注: "注", - 泪: "淚", - 泶: "澩", - 泷: "瀧", - 泸: "瀘", - 泺: "濼", - 泻: "瀉", - 泼: "潑", - 泽: "澤", - 泾: "涇", - 洁: "潔", - 洒: "灑", - 洼: "窪", - 浃: "浹", - 浅: "淺", - 浆: "漿", - 浇: "澆", - 浈: "湞", - 浉: "溮", - 浊: "濁", - 测: "測", - 浍: "澮", - 济: "濟", - 浏: "瀏", - 浐: "滻", - 浑: "渾", - 浒: "滸", - 浓: "濃", - 浔: "潯", - 浕: "濜", - 涂: "塗", - 涌: "湧", - 涚: "涗", - 涛: "濤", - 涝: "澇", - 涞: "淶", - 涟: "漣", - 涠: "潿", - 涡: "渦", - 涢: "溳", - 涣: "渙", - 涤: "滌", - 润: "潤", - 涧: "澗", - 涨: "漲", - 涩: "澀", - 淀: "澱", - 渊: "淵", - 渌: "淥", - 渍: "漬", - 渎: "瀆", - 渐: "漸", - 渑: "澠", - 渔: "漁", - 渖: "瀋", - 渗: "滲", - 温: "溫", - 游: "遊", - 湾: "灣", - 湿: "溼", - 溁: "濚", - 溃: "潰", - 溅: "濺", - 溆: "漵", - 溇: "漊", - 滗: "潷", - 滚: "滾", - 滞: "滯", - 滟: "灩", - 滠: "灄", - 满: "滿", - 滢: "瀅", - 滤: "濾", - 滥: "濫", - 滦: "灤", - 滨: "濱", - 滩: "灘", - 滪: "澦", - 漓: "漓", - 潆: "瀠", - 潇: "瀟", - 潋: "瀲", - 潍: "濰", - 潜: "潛", - 潴: "瀦", - 澛: "瀂", - 澜: "瀾", - 濑: "瀨", - 濒: "瀕", - 灏: "灝", - 灭: "滅", - 灯: "燈", - 灵: "靈", - 灶: "竈", - 灾: "災", - 灿: "燦", - 炀: "煬", - 炉: "爐", - 炖: "燉", - 炜: "煒", - 炝: "熗", - 点: "點", - 炼: "煉", - 炽: "熾", - 烁: "爍", - 烂: "爛", - 烃: "烴", - 烛: "燭", - 烟: "煙", - 烦: "煩", - 烧: "燒", - 烨: "燁", - 烩: "燴", - 烫: "燙", - 烬: "燼", - 热: "熱", - 焕: "煥", - 焖: "燜", - 焘: "燾", - 煴: "熅", - 熏: "燻", - 爱: "愛", - 爷: "爺", - 牍: "牘", - 牦: "犛", - 牵: "牽", - 牺: "犧", - 犊: "犢", - 状: "狀", - 犷: "獷", - 犸: "獁", - 犹: "猶", - 狈: "狽", - 狝: "獮", - 狞: "獰", - 独: "獨", - 狭: "狹", - 狮: "獅", - 狯: "獪", - 狰: "猙", - 狱: "獄", - 狲: "猻", - 猃: "獫", - 猎: "獵", - 猕: "獼", - 猡: "玀", - 猪: "豬", - 猫: "貓", - 猬: "蝟", - 献: "獻", - 獭: "獺", - 玑: "璣", - 玙: "璵", - 玚: "瑒", - 玛: "瑪", - 玩: "玩", - 玮: "瑋", - 环: "環", - 现: "現", - 玱: "瑲", - 玺: "璽", - 珐: "琺", - 珑: "瓏", - 珰: "璫", - 珲: "琿", - 琎: "璡", - 琏: "璉", - 琐: "瑣", - 琼: "瓊", - 瑶: "瑤", - 瑷: "璦", - 瑸: "璸", - 璇: "璇", - 璎: "瓔", - 瓒: "瓚", - 瓮: "甕", - 瓯: "甌", - 电: "電", - 画: "畫", - 畅: "暢", - 畴: "疇", - 疖: "癤", - 疗: "療", - 疟: "瘧", - 疠: "癘", - 疡: "瘍", - 疬: "癧", - 疭: "瘲", - 疮: "瘡", - 疯: "瘋", - 疱: "皰", - 疴: "痾", - 症: "症", - 痈: "癰", - 痉: "痙", - 痒: "癢", - 痖: "瘂", - 痨: "癆", - 痪: "瘓", - 痫: "癇", - 痴: "癡", - 瘅: "癉", - 瘆: "瘮", - 瘗: "瘞", - 瘘: "瘻", - 瘪: "癟", - 瘫: "癱", - 瘾: "癮", - 瘿: "癭", - 癞: "癩", - 癣: "癬", - 癫: "癲", - 皂: "皁", - 皑: "皚", - 皱: "皺", - 皲: "皸", - 盏: "盞", - 盐: "鹽", - 监: "監", - 盖: "蓋", - 盗: "盜", - 盘: "盤", - 眍: "瞘", - 眦: "眥", - 眬: "矓", - 睁: "睜", - 睐: "睞", - 睑: "瞼", - 瞆: "瞶", - 瞒: "瞞", - 瞩: "矚", - 矩: "矩", - 矫: "矯", - 矶: "磯", - 矾: "礬", - 矿: "礦", - 砀: "碭", - 码: "碼", - 砖: "磚", - 砗: "硨", - 砚: "硯", - 砜: "碸", - 砺: "礪", - 砻: "礱", - 砾: "礫", - 础: "礎", - 硁: "硜", - 硕: "碩", - 硖: "硤", - 硗: "磽", - 硙: "磑", - 硚: "礄", - 确: "確", - 硵: "磠", - 硷: "礆", - 碍: "礙", - 碛: "磧", - 碜: "磣", - 碱: "鹼", - 礼: "禮", - 祃: "禡", - 祎: "禕", - 祢: "禰", - 祯: "禎", - 祷: "禱", - 祸: "禍", - 禀: "稟", - 禄: "祿", - 禅: "禪", - 离: "離", - 私: "私", - 秃: "禿", - 秆: "稈", - 秋: "秋", - 种: "種", - 秘: "祕", - 积: "積", - 称: "稱", - 秽: "穢", - 秾: "穠", - 稆: "穭", - 税: "稅", - 稣: "穌", - 稳: "穩", - 穑: "穡", - 穞: "穭", - 穷: "窮", - 窃: "竊", - 窍: "竅", - 窎: "窵", - 窑: "窯", - 窜: "竄", - 窝: "窩", - 窥: "窺", - 窦: "竇", - 窭: "窶", - 竖: "豎", - 竞: "競", - 笃: "篤", - 笋: "筍", - 笔: "筆", - 笕: "筧", - 笺: "箋", - 笼: "籠", - 笾: "籩", - 筑: "築", - 筚: "篳", - 筛: "篩", - 筜: "簹", - 筝: "箏", - 筹: "籌", - 筼: "篔", - 签: "籤", - 筿: "篠", - 简: "簡", - 箓: "籙", - 箦: "簀", - 箧: "篋", - 箨: "籜", - 箩: "籮", - 箪: "簞", - 箫: "簫", - 篑: "簣", - 篓: "簍", - 篮: "籃", - 篯: "籛", - 篱: "籬", - 簖: "籪", - 籁: "籟", - 籴: "糴", - 类: "類", - 籼: "秈", - 粜: "糶", - 粝: "糲", - 粤: "粵", - 粪: "糞", - 粮: "糧", - 粽: "糉", - 糁: "糝", - 糇: "餱", - 糍: "餈", - 系: "系", - 紧: "緊", - 絷: "縶", - 緼: "縕", - 縆: "緪", - 纟: "糹", - 纠: "糾", - 纡: "紆", - 红: "紅", - 纣: "紂", - 纤: "纖", - 纥: "紇", - 约: "約", - 级: "級", - 纨: "紈", - 纩: "纊", - 纪: "紀", - 纫: "紉", - 纬: "緯", - 纭: "紜", - 纮: "紘", - 纯: "純", - 纰: "紕", - 纱: "紗", - 纲: "綱", - 纳: "納", - 纴: "紝", - 纵: "縱", - 纶: "綸", - 纷: "紛", - 纸: "紙", - 纹: "紋", - 纺: "紡", - 纻: "紵", - 纼: "紖", - 纽: "紐", - 纾: "紓", - 线: "線", - 绀: "紺", - 绁: "紲", - 绂: "紱", - 练: "練", - 组: "組", - 绅: "紳", - 细: "細", - 织: "織", - 终: "終", - 绉: "縐", - 绊: "絆", - 绋: "紼", - 绌: "絀", - 绍: "紹", - 绎: "繹", - 经: "經", - 绐: "紿", - 绑: "綁", - 绒: "絨", - 结: "結", - 绔: "絝", - 绕: "繞", - 绖: "絰", - 绗: "絎", - 绘: "繪", - 给: "給", - 绚: "絢", - 绛: "絳", - 络: "絡", - 绝: "絕", - 绞: "絞", - 统: "統", - 绠: "綆", - 绡: "綃", - 绢: "絹", - 绣: "繡", - 绤: "綌", - 绥: "綏", - 绦: "絛", - 继: "繼", - 绨: "綈", - 绩: "績", - 绪: "緒", - 绫: "綾", - 绬: "緓", - 续: "續", - 绮: "綺", - 绯: "緋", - 绰: "綽", - 绱: "鞝", - 绲: "緄", - 绳: "繩", - 维: "維", - 绵: "綿", - 绶: "綬", - 绷: "繃", - 绸: "綢", - 绹: "綯", - 绺: "綹", - 绻: "綣", - 综: "綜", - 绽: "綻", - 绾: "綰", - 绿: "綠", - 缀: "綴", - 缁: "緇", - 缂: "緙", - 缃: "緗", - 缄: "緘", - 缅: "緬", - 缆: "纜", - 缇: "緹", - 缈: "緲", - 缉: "緝", - 缊: "縕", - 缋: "繢", - 缌: "緦", - 缍: "綞", - 缎: "緞", - 缏: "緶", - 缐: "線", - 缑: "緱", - 缒: "縋", - 缓: "緩", - 缔: "締", - 缕: "縷", - 编: "編", - 缗: "緡", - 缘: "緣", - 缙: "縉", - 缚: "縛", - 缛: "縟", - 缜: "縝", - 缝: "縫", - 缞: "縗", - 缟: "縞", - 缠: "纏", - 缡: "縭", - 缢: "縊", - 缣: "縑", - 缤: "繽", - 缥: "縹", - 缦: "縵", - 缧: "縲", - 缨: "纓", - 缩: "縮", - 缪: "繆", - 缫: "繅", - 缬: "纈", - 缭: "繚", - 缮: "繕", - 缯: "繒", - 缰: "繮", - 缱: "繾", - 缲: "繰", - 缳: "繯", - 缴: "繳", - 缵: "纘", - 罂: "罌", - 网: "網", - 罗: "羅", - 罚: "罰", - 罢: "罷", - 罴: "羆", - 羁: "羈", - 羟: "羥", - 羡: "羨", - 群: "羣", - 翘: "翹", - 翙: "翽", - 翚: "翬", - 耢: "耮", - 耧: "耬", - 耸: "聳", - 耻: "恥", - 聂: "聶", - 聋: "聾", - 职: "職", - 聍: "聹", - 联: "聯", - 聩: "聵", - 聪: "聰", - 肃: "肅", - 肠: "腸", - 肤: "膚", - 肮: "骯", - 肴: "餚", - 肾: "腎", - 肿: "腫", - 胀: "脹", - 胁: "脅", - 胄: "胄", - 胆: "膽", - 背: "背", - 胜: "勝", - 胡: "胡", - 胧: "朧", - 胨: "腖", - 胪: "臚", - 胫: "脛", - 胶: "膠", - 脉: "脈", - 脍: "膾", - 脏: "髒", - 脐: "臍", - 脑: "腦", - 脓: "膿", - 脔: "臠", - 脚: "腳", - 脱: "脫", - 脶: "腡", - 脸: "臉", - 腊: "臘", - 腌: "醃", - 腘: "膕", - 腭: "齶", - 腻: "膩", - 腼: "靦", - 腽: "膃", - 腾: "騰", - 膑: "臏", - 膻: "羶", - 臜: "臢", - 致: "致", - 舆: "輿", - 舍: "舍", - 舣: "艤", - 舰: "艦", - 舱: "艙", - 舻: "艫", - 艰: "艱", - 艳: "豔", - 艺: "藝", - 节: "節", - 芈: "羋", - 芗: "薌", - 芜: "蕪", - 芦: "蘆", - 芸: "芸", - 苁: "蓯", - 苇: "葦", - 苈: "藶", - 苋: "莧", - 苌: "萇", - 苍: "蒼", - 苎: "苧", - 苏: "蘇", - 苔: "苔", - 苧: "薴", - 苹: "蘋", - 范: "範", - 茎: "莖", - 茏: "蘢", - 茑: "蔦", - 茔: "塋", - 茕: "煢", - 茧: "繭", - 荆: "荊", - 荐: "薦", - 荙: "薘", - 荚: "莢", - 荛: "蕘", - 荜: "蓽", - 荝: "萴", - 荞: "蕎", - 荟: "薈", - 荠: "薺", - 荡: "蕩", - 荣: "榮", - 荤: "葷", - 荥: "滎", - 荦: "犖", - 荧: "熒", - 荨: "蕁", - 荩: "藎", - 荪: "蓀", - 荫: "蔭", - 荬: "蕒", - 荭: "葒", - 荮: "葤", - 药: "藥", - 莅: "蒞", - 莱: "萊", - 莲: "蓮", - 莳: "蒔", - 莴: "萵", - 莶: "薟", - 获: "獲", - 莸: "蕕", - 莹: "瑩", - 莺: "鶯", - 莼: "蓴", - 萚: "蘀", - 萝: "蘿", - 萤: "螢", - 营: "營", - 萦: "縈", - 萧: "蕭", - 萨: "薩", - 葱: "蔥", - 蒀: "蒕", - 蒇: "蕆", - 蒉: "蕢", - 蒋: "蔣", - 蒌: "蔞", - 蒏: "醟", - 蒙: "蒙", - 蓝: "藍", - 蓟: "薊", - 蓠: "蘺", - 蓣: "蕷", - 蓥: "鎣", - 蓦: "驀", - 蔂: "虆", - 蔑: "蔑", - 蔷: "薔", - 蔹: "蘞", - 蔺: "藺", - 蔼: "藹", - 蕰: "薀", - 蕲: "蘄", - 蕴: "蘊", - 薮: "藪", - 藓: "蘚", - 藴: "蘊", - 蘖: "櫱", - 虏: "虜", - 虑: "慮", - 虚: "虛", - 虫: "蟲", - 虬: "虯", - 虮: "蟣", - 虱: "蝨", - 虽: "雖", - 虾: "蝦", - 虿: "蠆", - 蚀: "蝕", - 蚁: "蟻", - 蚂: "螞", - 蚃: "蠁", - 蚕: "蠶", - 蚝: "蠔", - 蚬: "蜆", - 蛊: "蠱", - 蛎: "蠣", - 蛏: "蟶", - 蛮: "蠻", - 蛰: "蟄", - 蛱: "蛺", - 蛲: "蟯", - 蛳: "螄", - 蛴: "蠐", - 蜕: "蛻", - 蜗: "蝸", - 蜡: "蠟", - 蝇: "蠅", - 蝈: "蟈", - 蝉: "蟬", - 蝎: "蠍", - 蝼: "螻", - 蝾: "蠑", - 螀: "螿", - 螨: "蟎", - 蟏: "蠨", - 衅: "釁", - 衔: "銜", - 补: "補", - 表: "表", - 衬: "襯", - 衮: "袞", - 袄: "襖", - 袅: "嫋", - 袆: "褘", - 袜: "襪", - 袭: "襲", - 袯: "襏", - 装: "裝", - 裆: "襠", - 裈: "褌", - 裢: "褳", - 裣: "襝", - 裤: "褲", - 裥: "襉", - 褛: "褸", - 褴: "襤", - 襕: "襴", - 见: "見", - 观: "觀", - 觃: "覎", - 规: "規", - 觅: "覓", - 视: "視", - 觇: "覘", - 览: "覽", - 觉: "覺", - 觊: "覬", - 觋: "覡", - 觌: "觿", - 觍: "覥", - 觎: "覦", - 觏: "覯", - 觐: "覲", - 觑: "覷", - 觞: "觴", - 触: "觸", - 觯: "觶", - 訚: "誾", - 詟: "讋", - 誉: "譽", - 誊: "謄", - 讠: "訁", - 计: "計", - 订: "訂", - 讣: "訃", - 认: "認", - 讥: "譏", - 讦: "訐", - 讧: "訌", - 讨: "討", - 让: "讓", - 讪: "訕", - 讫: "訖", - 讬: "託", - 训: "訓", - 议: "議", - 讯: "訊", - 记: "記", - 讱: "訒", - 讲: "講", - 讳: "諱", - 讴: "謳", - 讵: "詎", - 讶: "訝", - 讷: "訥", - 许: "許", - 讹: "訛", - 论: "論", - 讻: "訩", - 讼: "訟", - 讽: "諷", - 设: "設", - 访: "訪", - 诀: "訣", - 证: "證", - 诂: "詁", - 诃: "訶", - 评: "評", - 诅: "詛", - 识: "識", - 诇: "詗", - 诈: "詐", - 诉: "訴", - 诊: "診", - 诋: "詆", - 诌: "謅", - 词: "詞", - 诎: "詘", - 诏: "詔", - 诐: "詖", - 译: "譯", - 诒: "詒", - 诓: "誆", - 诔: "誄", - 试: "試", - 诖: "詿", - 诗: "詩", - 诘: "詰", - 诙: "詼", - 诚: "誠", - 诛: "誅", - 诜: "詵", - 话: "話", - 诞: "誕", - 诟: "詬", - 诠: "詮", - 诡: "詭", - 询: "詢", - 诣: "詣", - 诤: "諍", - 该: "該", - 详: "詳", - 诧: "詫", - 诨: "諢", - 诩: "詡", - 诪: "譸", - 诫: "誡", - 诬: "誣", - 语: "語", - 诮: "誚", - 误: "誤", - 诰: "誥", - 诱: "誘", - 诲: "誨", - 诳: "誑", - 说: "說", - 诵: "誦", - 诶: "誒", - 请: "請", - 诸: "諸", - 诹: "諏", - 诺: "諾", - 读: "讀", - 诼: "諑", - 诽: "誹", - 课: "課", - 诿: "諉", - 谀: "諛", - 谁: "誰", - 谂: "諗", - 调: "調", - 谄: "諂", - 谅: "諒", - 谆: "諄", - 谇: "誶", - 谈: "談", - 谉: "讅", - 谊: "誼", - 谋: "謀", - 谌: "諶", - 谍: "諜", - 谎: "謊", - 谏: "諫", - 谐: "諧", - 谑: "謔", - 谒: "謁", - 谓: "謂", - 谔: "諤", - 谕: "諭", - 谖: "諼", - 谗: "讒", - 谘: "諮", - 谙: "諳", - 谚: "諺", - 谛: "諦", - 谜: "謎", - 谝: "諞", - 谞: "諝", - 谟: "謨", - 谠: "讜", - 谡: "謖", - 谢: "謝", - 谣: "謠", - 谤: "謗", - 谥: "諡", - 谦: "謙", - 谧: "謐", - 谨: "謹", - 谩: "謾", - 谪: "謫", - 谫: "譾", - 谬: "謬", - 谭: "譚", - 谮: "譖", - 谯: "譙", - 谰: "讕", - 谱: "譜", - 谲: "譎", - 谳: "讞", - 谴: "譴", - 谵: "譫", - 谶: "讖", - 谷: "谷", - 豮: "豶", - 贝: "貝", - 贞: "貞", - 负: "負", - 贠: "貟", - 贡: "貢", - 财: "財", - 责: "責", - 贤: "賢", - 败: "敗", - 账: "賬", - 货: "貨", - 质: "質", - 贩: "販", - 贪: "貪", - 贫: "貧", - 贬: "貶", - 购: "購", - 贮: "貯", - 贯: "貫", - 贰: "貳", - 贱: "賤", - 贲: "賁", - 贳: "貰", - 贴: "貼", - 贵: "貴", - 贶: "貺", - 贷: "貸", - 贸: "貿", - 费: "費", - 贺: "賀", - 贻: "貽", - 贼: "賊", - 贽: "贄", - 贾: "賈", - 贿: "賄", - 赀: "貲", - 赁: "賃", - 赂: "賂", - 赃: "贓", - 资: "資", - 赅: "賅", - 赆: "贐", - 赇: "賕", - 赈: "賑", - 赉: "賚", - 赊: "賒", - 赋: "賦", - 赌: "賭", - 赍: "齎", - 赎: "贖", - 赏: "賞", - 赐: "賜", - 赑: "贔", - 赒: "賙", - 赓: "賡", - 赔: "賠", - 赕: "賧", - 赖: "賴", - 赗: "賵", - 赘: "贅", - 赙: "賻", - 赚: "賺", - 赛: "賽", - 赜: "賾", - 赝: "贗", - 赞: "贊", - 赟: "贇", - 赠: "贈", - 赡: "贍", - 赢: "贏", - 赣: "贛", - 赪: "赬", - 赵: "趙", - 赶: "趕", - 趋: "趨", - 趱: "趲", - 趸: "躉", - 跃: "躍", - 跄: "蹌", - 跖: "蹠", - 跞: "躒", - 践: "踐", - 跶: "躂", - 跷: "蹺", - 跸: "蹕", - 跹: "躚", - 跻: "躋", - 踌: "躊", - 踪: "蹤", - 踬: "躓", - 踯: "躑", - 蹑: "躡", - 蹒: "蹣", - 蹰: "躕", - 蹿: "躥", - 躏: "躪", - 躜: "躦", - 躯: "軀", - 輼: "轀", - 车: "車", - 轧: "軋", - 轨: "軌", - 轩: "軒", - 轪: "軑", - 轫: "軔", - 转: "轉", - 轭: "軛", - 轮: "輪", - 软: "軟", - 轰: "轟", - 轱: "軲", - 轲: "軻", - 轳: "轤", - 轴: "軸", - 轵: "軹", - 轶: "軼", - 轷: "軤", - 轸: "軫", - 轹: "轢", - 轺: "軺", - 轻: "輕", - 轼: "軾", - 载: "載", - 轾: "輊", - 轿: "轎", - 辀: "輈", - 辁: "輇", - 辂: "輅", - 较: "較", - 辄: "輒", - 辅: "輔", - 辆: "輛", - 辇: "輦", - 辈: "輩", - 辉: "輝", - 辊: "輥", - 辋: "輞", - 辌: "輬", - 辍: "輟", - 辎: "輜", - 辏: "輳", - 辐: "輻", - 辑: "輯", - 辒: "轀", - 输: "輸", - 辔: "轡", - 辕: "轅", - 辖: "轄", - 辗: "輾", - 辘: "轆", - 辙: "轍", - 辚: "轔", - 辞: "辭", - 辟: "闢", - 辩: "辯", - 辫: "辮", - 边: "邊", - 辽: "遼", - 达: "達", - 迁: "遷", - 过: "過", - 迈: "邁", - 运: "運", - 还: "還", - 这: "這", - 进: "進", - 远: "遠", - 违: "違", - 连: "連", - 迟: "遲", - 迩: "邇", - 迳: "逕", - 迹: "跡", - 适: "適", - 选: "選", - 逊: "遜", - 递: "遞", - 逦: "邐", - 逻: "邏", - 遗: "遺", - 遥: "遙", - 邓: "鄧", - 邝: "鄺", - 邬: "鄔", - 邮: "郵", - 邹: "鄒", - 邺: "鄴", - 邻: "鄰", - 郁: "鬱", - 郏: "郟", - 郐: "鄶", - 郑: "鄭", - 郓: "鄆", - 郦: "酈", - 郧: "鄖", - 郸: "鄲", - 酂: "酇", - 酝: "醞", - 酦: "醱", - 酱: "醬", - 酸: "酸", - 酽: "釅", - 酾: "釃", - 酿: "釀", - 醖: "醞", - 采: "採", - 释: "釋", - 里: "裏", - 鉴: "鑑", - 銮: "鑾", - 錾: "鏨", - 钅: "釒", - 钆: "釓", - 钇: "釔", - 针: "針", - 钉: "釘", - 钊: "釗", - 钋: "釙", - 钌: "釕", - 钍: "釷", - 钎: "釺", - 钏: "釧", - 钐: "釤", - 钑: "鈒", - 钒: "釩", - 钓: "釣", - 钔: "鍆", - 钕: "釹", - 钖: "鍚", - 钗: "釵", - 钘: "鈃", - 钙: "鈣", - 钚: "鈈", - 钛: "鈦", - 钜: "鉅", - 钝: "鈍", - 钞: "鈔", - 钟: "鍾", - 钠: "鈉", - 钡: "鋇", - 钢: "鋼", - 钣: "鈑", - 钤: "鈐", - 钥: "鑰", - 钦: "欽", - 钧: "鈞", - 钨: "鎢", - 钩: "鉤", - 钪: "鈧", - 钫: "鈁", - 钬: "鈥", - 钭: "鈄", - 钮: "鈕", - 钯: "鈀", - 钰: "鈺", - 钱: "錢", - 钲: "鉦", - 钳: "鉗", - 钴: "鈷", - 钵: "鉢", - 钶: "鈳", - 钷: "鉕", - 钸: "鈽", - 钹: "鈸", - 钺: "鉞", - 钻: "鑽", - 钼: "鉬", - 钽: "鉭", - 钾: "鉀", - 钿: "鈿", - 铀: "鈾", - 铁: "鐵", - 铂: "鉑", - 铃: "鈴", - 铄: "鑠", - 铅: "鉛", - 铆: "鉚", - 铇: "鉋", - 铈: "鈰", - 铉: "鉉", - 铊: "鉈", - 铋: "鉍", - 铌: "鈮", - 铍: "鈹", - 铎: "鐸", - 铏: "鉶", - 铐: "銬", - 铑: "銠", - 铒: "鉺", - 铓: "鋩", - 铔: "錏", - 铕: "銪", - 铖: "鋮", - 铗: "鋏", - 铘: "鋣", - 铙: "鐃", - 铚: "銍", - 铛: "鐺", - 铜: "銅", - 铝: "鋁", - 铞: "銱", - 铟: "銦", - 铠: "鎧", - 铡: "鍘", - 铢: "銖", - 铣: "銑", - 铤: "鋌", - 铥: "銩", - 铦: "銛", - 铧: "鏵", - 铨: "銓", - 铩: "鎩", - 铪: "鉿", - 铫: "銚", - 铬: "鉻", - 铭: "銘", - 铮: "錚", - 铯: "銫", - 铰: "鉸", - 铱: "銥", - 铲: "鏟", - 铳: "銃", - 铴: "鐋", - 铵: "銨", - 银: "銀", - 铷: "銣", - 铸: "鑄", - 铹: "鐒", - 铺: "鋪", - 铻: "鋙", - 铼: "錸", - 铽: "鋱", - 链: "鏈", - 铿: "鏗", - 销: "銷", - 锁: "鎖", - 锂: "鋰", - 锃: "鋥", - 锄: "鋤", - 锅: "鍋", - 锆: "鋯", - 锇: "鋨", - 锈: "鏽", - 锉: "銼", - 锊: "鋝", - 锋: "鋒", - 锌: "鋅", - 锍: "鋶", - 锎: "鐦", - 锏: "鐧", - 锐: "銳", - 锑: "銻", - 锒: "鋃", - 锓: "鋟", - 锔: "鋦", - 锕: "錒", - 锖: "錆", - 锗: "鍺", - 锘: "鍩", - 错: "錯", - 锚: "錨", - 锛: "錛", - 锜: "錡", - 锝: "鍀", - 锞: "錁", - 锟: "錕", - 锠: "錩", - 锡: "錫", - 锢: "錮", - 锣: "鑼", - 锤: "錘", - 锥: "錐", - 锦: "錦", - 锧: "鑕", - 锨: "鍁", - 锩: "錈", - 锪: "鍃", - 锫: "錇", - 锬: "錟", - 锭: "錠", - 键: "鍵", - 锯: "鋸", - 锰: "錳", - 锱: "錙", - 锲: "鍥", - 锳: "鍈", - 锴: "鍇", - 锵: "鏘", - 锶: "鍶", - 锷: "鍔", - 锸: "鍤", - 锹: "鍬", - 锺: "鍾", - 锻: "鍛", - 锼: "鎪", - 锽: "鍠", - 锾: "鍰", - 锿: "鎄", - 镀: "鍍", - 镁: "鎂", - 镂: "鏤", - 镃: "鎡", - 镄: "鐨", - 镅: "鎇", - 镆: "鏌", - 镇: "鎮", - 镈: "鎛", - 镉: "鎘", - 镊: "鑷", - 镋: "钂", - 镌: "鐫", - 镍: "鎳", - 镎: "鎿", - 镏: "鎦", - 镐: "鎬", - 镑: "鎊", - 镒: "鎰", - 镓: "鎵", - 镔: "鑌", - 镕: "鎔", - 镖: "鏢", - 镗: "鏜", - 镘: "鏝", - 镙: "鏍", - 镚: "鏰", - 镛: "鏞", - 镜: "鏡", - 镝: "鏑", - 镞: "鏃", - 镟: "鏇", - 镠: "鏐", - 镡: "鐔", - 镢: "钁", - 镣: "鐐", - 镤: "鏷", - 镥: "鑥", - 镦: "鐓", - 镧: "鑭", - 镨: "鐠", - 镩: "鑹", - 镪: "鏹", - 镫: "鐙", - 镬: "鑊", - 镭: "鐳", - 镮: "鐶", - 镯: "鐲", - 镰: "鐮", - 镱: "鐿", - 镲: "鑔", - 镳: "鑣", - 镴: "鑞", - 镵: "鑱", - 镶: "鑲", - 长: "長", - 门: "門", - 闩: "閂", - 闪: "閃", - 闫: "閆", - 闬: "閈", - 闭: "閉", - 问: "問", - 闯: "闖", - 闰: "閏", - 闱: "闈", - 闲: "閒", - 闳: "閎", - 间: "間", - 闵: "閔", - 闶: "閌", - 闷: "悶", - 闸: "閘", - 闹: "鬧", - 闺: "閨", - 闻: "聞", - 闼: "闥", - 闽: "閩", - 闾: "閭", - 闿: "闓", - 阀: "閥", - 阁: "閣", - 阂: "閡", - 阃: "閫", - 阄: "鬮", - 阅: "閱", - 阆: "閬", - 阇: "闍", - 阈: "閾", - 阉: "閹", - 阊: "閶", - 阋: "鬩", - 阌: "閿", - 阍: "閽", - 阎: "閻", - 阏: "閼", - 阐: "闡", - 阑: "闌", - 阒: "闃", - 阓: "闠", - 阔: "闊", - 阕: "闋", - 阖: "闔", - 阗: "闐", - 阘: "闒", - 阙: "闕", - 阚: "闞", - 阛: "闤", - 队: "隊", - 阳: "陽", - 阴: "陰", - 阵: "陣", - 阶: "階", - 际: "際", - 陆: "陸", - 陇: "隴", - 陈: "陳", - 陉: "陘", - 陕: "陝", - 陦: "隯", - 陧: "隉", - 陨: "隕", - 险: "險", - 随: "隨", - 隐: "隱", - 隶: "隸", - 隽: "雋", - 难: "難", - 雇: "僱", - 雏: "雛", - 雕: "雕", - 雠: "讎", - 雳: "靂", - 雾: "霧", - 霁: "霽", - 霉: "黴", - 霡: "霢", - 霭: "靄", - 靓: "靚", - 靔: "靝", - 静: "靜", - 面: "面", - 靥: "靨", - 鞑: "韃", - 鞒: "鞽", - 鞯: "韉", - 鞲: "韝", - 韦: "韋", - 韧: "韌", - 韨: "韍", - 韩: "韓", - 韪: "韙", - 韫: "韞", - 韬: "韜", - 韵: "韻", - 页: "頁", - 顶: "頂", - 顷: "頃", - 顸: "頇", - 项: "項", - 顺: "順", - 须: "須", - 顼: "頊", - 顽: "頑", - 顾: "顧", - 顿: "頓", - 颀: "頎", - 颁: "頒", - 颂: "頌", - 颃: "頏", - 预: "預", - 颅: "顱", - 领: "領", - 颇: "頗", - 颈: "頸", - 颉: "頡", - 颊: "頰", - 颋: "頲", - 颌: "頜", - 颍: "潁", - 颎: "熲", - 颏: "頦", - 颐: "頤", - 频: "頻", - 颒: "頮", - 颓: "頹", - 颔: "頷", - 颕: "頴", - 颖: "穎", - 颗: "顆", - 题: "題", - 颙: "顒", - 颚: "顎", - 颛: "顓", - 颜: "顏", - 额: "額", - 颞: "顳", - 颟: "顢", - 颠: "顛", - 颡: "顙", - 颢: "顥", - 颣: "纇", - 颤: "顫", - 颥: "顬", - 颦: "顰", - 颧: "顴", - 风: "風", - 飏: "颺", - 飐: "颭", - 飑: "颮", - 飒: "颯", - 飓: "颶", - 飔: "颸", - 飕: "颼", - 飖: "颻", - 飗: "飀", - 飘: "飄", - 飙: "飆", - 飚: "飈", - 飞: "飛", - 飨: "饗", - 餍: "饜", - 饣: "飠", - 饤: "飣", - 饥: "飢", - 饦: "飥", - 饧: "餳", - 饨: "飩", - 饩: "餼", - 饪: "飪", - 饫: "飫", - 饬: "飭", - 饭: "飯", - 饮: "飲", - 饯: "餞", - 饰: "飾", - 饱: "飽", - 饲: "飼", - 饳: "飿", - 饴: "飴", - 饵: "餌", - 饶: "饒", - 饷: "餉", - 饸: "餄", - 饹: "餎", - 饺: "餃", - 饻: "餏", - 饼: "餅", - 饽: "餑", - 饾: "餖", - 饿: "餓", - 馀: "餘", - 馁: "餒", - 馂: "餕", - 馃: "餜", - 馄: "餛", - 馅: "餡", - 馆: "館", - 馇: "餷", - 馈: "饋", - 馉: "餶", - 馊: "餿", - 馋: "饞", - 馌: "饁", - 馍: "饃", - 馎: "餺", - 馏: "餾", - 馐: "饈", - 馑: "饉", - 馒: "饅", - 馓: "饊", - 馔: "饌", - 馕: "饢", - 马: "馬", - 驭: "馭", - 驮: "馱", - 驯: "馴", - 驰: "馳", - 驱: "驅", - 驲: "馹", - 驳: "駁", - 驴: "驢", - 驵: "駔", - 驶: "駛", - 驷: "駟", - 驸: "駙", - 驹: "駒", - 驺: "騶", - 驻: "駐", - 驼: "駝", - 驽: "駑", - 驾: "駕", - 驿: "驛", - 骀: "駘", - 骁: "驍", - 骂: "罵", - 骃: "駰", - 骄: "驕", - 骅: "驊", - 骆: "駱", - 骇: "駭", - 骈: "駢", - 骉: "驫", - 骊: "驪", - 骋: "騁", - 验: "驗", - 骍: "騂", - 骎: "駸", - 骏: "駿", - 骐: "騏", - 骑: "騎", - 骒: "騍", - 骓: "騅", - 骔: "騌", - 骕: "驌", - 骖: "驂", - 骗: "騙", - 骘: "騭", - 骙: "騤", - 骚: "騷", - 骛: "騖", - 骜: "驁", - 骝: "騮", - 骞: "騫", - 骟: "騸", - 骠: "驃", - 骡: "騾", - 骢: "驄", - 骣: "驏", - 骤: "驟", - 骥: "驥", - 骦: "驦", - 骧: "驤", - 髅: "髏", - 髋: "髖", - 髌: "髕", - 鬓: "鬢", - 鬶: "鬹", - 魇: "魘", - 魉: "魎", - 鱼: "魚", - 鱽: "魛", - 鱾: "魢", - 鱿: "魷", - 鲀: "魨", - 鲁: "魯", - 鲂: "魴", - 鲃: "䰾", - 鲄: "魺", - 鲅: "鮁", - 鲆: "鮃", - 鲇: "鮎", - 鲈: "鱸", - 鲉: "鮋", - 鲊: "鮓", - 鲋: "鮒", - 鲌: "鮊", - 鲍: "鮑", - 鲎: "鱟", - 鲏: "鮍", - 鲐: "鮐", - 鲑: "鮭", - 鲒: "鮚", - 鲓: "鮳", - 鲔: "鮪", - 鲕: "鮞", - 鲖: "鮦", - 鲗: "鰂", - 鲘: "鮜", - 鲙: "鱠", - 鲚: "鱭", - 鲛: "鮫", - 鲜: "鮮", - 鲝: "鮺", - 鲞: "鯗", - 鲟: "鱘", - 鲠: "鯁", - 鲡: "鱺", - 鲢: "鰱", - 鲣: "鰹", - 鲤: "鯉", - 鲥: "鰣", - 鲦: "鰷", - 鲧: "鯀", - 鲨: "鯊", - 鲩: "鯇", - 鲪: "鮶", - 鲫: "鯽", - 鲬: "鯒", - 鲭: "鯖", - 鲮: "鯪", - 鲯: "鯕", - 鲰: "鯫", - 鲱: "鯡", - 鲲: "鯤", - 鲳: "鯧", - 鲴: "鯝", - 鲵: "鯢", - 鲶: "鯰", - 鲷: "鯛", - 鲸: "鯨", - 鲹: "鰺", - 鲺: "鯴", - 鲻: "鯔", - 鲼: "鱝", - 鲽: "鰈", - 鲾: "鰏", - 鲿: "鱨", - 鳀: "鯷", - 鳁: "鰮", - 鳂: "鰃", - 鳃: "鰓", - 鳄: "鱷", - 鳅: "鰍", - 鳆: "鰒", - 鳇: "鰉", - 鳈: "鰁", - 鳉: "鱂", - 鳊: "鯿", - 鳋: "鰠", - 鳌: "鰲", - 鳍: "鰭", - 鳎: "鰨", - 鳏: "鰥", - 鳐: "鰩", - 鳑: "鰟", - 鳒: "鰜", - 鳓: "鰳", - 鳔: "鰾", - 鳕: "鱈", - 鳖: "鱉", - 鳗: "鰻", - 鳘: "鰵", - 鳙: "鱅", - 鳚: "䲁", - 鳛: "鰼", - 鳜: "鱖", - 鳝: "鱔", - 鳞: "鱗", - 鳟: "鱒", - 鳠: "鱯", - 鳡: "鱤", - 鳢: "鱧", - 鳣: "鱣", - 鳤: "䲘", - 鸟: "鳥", - 鸠: "鳩", - 鸡: "雞", - 鸢: "鳶", - 鸣: "鳴", - 鸤: "鳲", - 鸥: "鷗", - 鸦: "鴉", - 鸧: "鶬", - 鸨: "鴇", - 鸩: "鴆", - 鸪: "鴣", - 鸫: "鶇", - 鸬: "鸕", - 鸭: "鴨", - 鸮: "鴞", - 鸯: "鴦", - 鸰: "鴒", - 鸱: "鴟", - 鸲: "鴝", - 鸳: "鴛", - 鸴: "鷽", - 鸵: "鴕", - 鸶: "鷥", - 鸷: "鷙", - 鸸: "鴯", - 鸹: "鴰", - 鸺: "鵂", - 鸻: "鴴", - 鸼: "鵃", - 鸽: "鴿", - 鸾: "鸞", - 鸿: "鴻", - 鹀: "鵐", - 鹁: "鵓", - 鹂: "鸝", - 鹃: "鵑", - 鹄: "鵠", - 鹅: "鵝", - 鹆: "鵒", - 鹇: "鷳", - 鹈: "鵜", - 鹉: "鵡", - 鹊: "鵲", - 鹋: "鶓", - 鹌: "鵪", - 鹍: "鵾", - 鹎: "鵯", - 鹏: "鵬", - 鹐: "鵮", - 鹑: "鶉", - 鹒: "鶊", - 鹓: "鵷", - 鹔: "鷫", - 鹕: "鶘", - 鹖: "鶡", - 鹗: "鶚", - 鹘: "鶻", - 鹙: "鶖", - 鹚: "鷀", - 鹛: "鶥", - 鹜: "鶩", - 鹝: "鷊", - 鹞: "鷂", - 鹟: "鶲", - 鹠: "鶹", - 鹡: "鶺", - 鹢: "鷁", - 鹣: "鶼", - 鹤: "鶴", - 鹥: "鷖", - 鹦: "鸚", - 鹧: "鷓", - 鹨: "鷚", - 鹩: "鷯", - 鹪: "鷦", - 鹫: "鷲", - 鹬: "鷸", - 鹭: "鷺", - 鹮: "䴉", - 鹯: "鸇", - 鹰: "鷹", - 鹱: "鸌", - 鹲: "鸏", - 鹳: "鸛", - 鹴: "鸘", - 鹾: "鹺", - 麦: "麥", - 麸: "麩", - 麹: "麴", - 麺: "麪", - 麽: "麼", - 黄: "黃", - 黉: "黌", - 黡: "黶", - 黩: "黷", - 黪: "黲", - 黾: "黽", - 鼋: "黿", - 鼌: "鼂", - 鼍: "鼉", - 鼹: "鼴", - 齐: "齊", - 齑: "齏", - 齿: "齒", - 龀: "齔", - 龁: "齕", - 龂: "齗", - 龃: "齟", - 龄: "齡", - 龅: "齙", - 龆: "齠", - 龇: "齜", - 龈: "齦", - 龉: "齬", - 龊: "齪", - 龋: "齲", - 龌: "齷", - 龙: "龍", - 龚: "龔", - 龛: "龕", - 龟: "龜", - 鿎: "䃮", - 鿏: "䥑", - 鿒: "鿓", - 鿔: "鎶", - "𠀾": "𠁞", - "𠆲": "儣", - "𠆿": "𠌥", - "𠇹": "俓", - "𠉂": "㒓", - "𠉗": "𠏢", - "𠋆": "儭", - "𠚳": "𠠎", - "𠛅": "剾", - "𠛆": "𠞆", - "𠛾": "𪟖", - "𠡠": "勑", - "𠮶": "嗰", - "𠯟": "哯", - "𠯠": "噅", - "𠰱": "㘉", - "𠰷": "嚧", - "𠱞": "囃", - "𠲥": "𡅏", - "𠴛": "𡃕", - "𠴢": "𡄔", - "𠵸": "𡄣", - "𠵾": "㗲", - "𡋀": "𡓾", - "𡋗": "𡑭", - "𡋤": "壗", - "𡍣": "𡔖", - "𡒄": "壈", - "𡝠": "㜷", - "𡞋": "㜗", - "𡞱": "㜢", - "𡠟": "孎", - "𡥧": "孻", - "𡭜": "𡮉", - "𡭬": "𡮣", - "𡳃": "𡳳", - "𡳒": "𦘧", - "𡶴": "嵼", - "𡸃": "𡽗", - "𡺃": "嶈", - "𡺄": "嶘", - "𢋈": "㢝", - "𢗓": "㦛", - "𢘙": "𢤱", - "𢘝": "𢣚", - "𢘞": "𢣭", - "𢙏": "愻", - "𢙐": "憹", - "𢙑": "𢠼", - "𢙒": "憢", - "𢙓": "懀", - "𢛯": "㦎", - "𢠁": "懎", - "𢢐": "𤢻", - "𢧐": "戰", - "𢫊": "𢷮", - "𢫞": "𢶫", - "𢫬": "摋", - "𢬍": "擫", - "𢬦": "𢹿", - "𢭏": "擣", - "𢽾": "斅", - "𣃁": "斸", - "𣆐": "曥", - "𣈣": "𣋋", - "𣍨": "𦢈", - "𣍯": "腪", - "𣍰": "脥", - "𣎑": "臗", - "𣏢": "槫", - "𣐕": "桱", - "𣐤": "欍", - "𣑶": "𣠲", - "𣒌": "楇", - "𣓿": "橯", - "𣔌": "樤", - "𣗊": "樠", - "𣗋": "欓", - "𣗙": "㰙", - "𣘐": "㯤", - "𣘓": "𣞻", - "𣘴": "檭", - "𣘷": "𣝕", - "𣚚": "欘", - "𣞎": "𣠩", - "𣨼": "殢", - "𣭤": "𣯴", - "𣯣": "𣯩", - "𣱝": "氭", - "𣲗": "湋", - "𣲘": "潕", - "𣳆": "㵗", - "𣶩": "澅", - "𣶫": "𣿉", - "𣶭": "𪷓", - "𣷷": "𤅶", - "𣸣": "濆", - "𣺼": "灙", - "𣺽": "𤁣", - "𣽷": "瀃", - "𤆡": "熓", - "𤆢": "㷍", - "𤇃": "爄", - "𤇄": "熌", - "𤇭": "爖", - "𤇹": "熚", - "𤈶": "熉", - "𤈷": "㷿", - "𤊀": "𤒎", - "𤊰": "𤓩", - "𤋏": "熡", - "𤎺": "𤓎", - "𤎻": "𤑳", - "𤙯": "𤛮", - "𤝢": "𤢟", - "𤞃": "獩", - "𤞤": "玁", - "𤠋": "㺏", - "𤦀": "瓕", - "𤩽": "瓛", - "𤳄": "𤳸", - "𤶊": "癐", - "𤶧": "𤸫", - "𤻊": "㿗", - "𤽯": "㿧", - "𤾀": "皟", - "𤿲": "麬", - "𥁢": "䀉", - "𥅘": "𥌃", - "𥅴": "䀹", - "𥅿": "𥊝", - "𥆧": "瞤", - "𥇢": "䁪", - "𥎝": "䂎", - "𥐟": "礒", - "𥐯": "𥖅", - "𥐰": "𥕥", - "𥐻": "碙", - "𥞦": "𥞵", - "𥧂": "𥨐", - "𥩟": "竚", - "𥩺": "𥪂", - "𥫣": "籅", - "𥬀": "䉙", - "𥬞": "籋", - "𥬠": "篘", - "𥭉": "𥵊", - "𥮋": "𥸠", - "𥮜": "䉲", - "𥮾": "篸", - "𥱔": "𥵃", - "𥹥": "𥼽", - "𥺅": "䊭", - "𥺇": "𥽖", - "𦈈": "𥿊", - "𦈉": "緷", - "𦈋": "綇", - "𦈌": "綀", - "𦈎": "繟", - "𦈏": "緍", - "𦈐": "縺", - "𦈑": "緸", - "𦈒": "𦂅", - "𦈓": "䋿", - "𦈔": "縎", - "𦈕": "緰", - "𦈖": "䌈", - "𦈗": "𦃄", - "𦈘": "䌋", - "𦈙": "䌰", - "𦈚": "縬", - "𦈛": "繓", - "𦈜": "䌖", - "𦈝": "繏", - "𦈞": "䌟", - "𦈟": "䌝", - "𦈠": "䌥", - "𦈡": "繻", - "𦍠": "䍽", - "𦛨": "朥", - "𦝼": "膢", - "𦟗": "𦣎", - "𦨩": "𦪽", - "𦰏": "蓧", - "𦰴": "䕳", - "𦶟": "爇", - "𦶻": "𦾟", - "𦻕": "蘟", - "𧉐": "𧕟", - "𧉞": "䗿", - "𧌥": "𧎈", - "𧏖": "蠙", - "𧏗": "蠀", - "𧑏": "蠾", - "𧒭": "𧔥", - "𧜭": "䙱", - "𧝝": "襰", - "𧝧": "𧟀", - "𧮪": "詀", - "𧳕": "𧳟", - "𧹑": "䞈", - "𧹒": "買", - "𧹓": "𧶔", - "𧹔": "賬", - "𧹕": "", - "𧹖": "賟", - "𧹗": "贃", - "𧿈": "𨇁", - "𨀁": "躘", - "𨀱": "𨄣", - "𨁴": "𨅍", - "𨂺": "𨈊", - "𨄄": "𨈌", - "𨅛": "䠱", - "𨅫": "𨇞", - "𨅬": "躝", - "𨉗": "軉", - "𨐅": "軗", - "𨐆": "𨊻", - "𨐇": "𨏠", - "𨐈": "輄", - "𨐉": "𨎮", - "𨐊": "𨏥", - "𨑹": "䢨", - "𨟳": "𨣞", - "𨠨": "𨣧", - "𨡙": "𨢿", - "𨡺": "𨣈", - "𨤰": "𨤻", - "𨰾": "鎷", - "𨰿": "釳", - "𨱀": "𨥛", - "𨱁": "鈠", - "𨱂": "鈋", - "𨱃": "鈲", - "𨱄": "鈯", - "𨱅": "鉁", - "𨱆": "龯", - "𨱇": "銶", - "𨱈": "鋉", - "𨱉": "鍄", - "𨱊": "𨧱", - "𨱋": "錂", - "𨱌": "鏆", - "𨱍": "鎯", - "𨱎": "鍮", - "𨱏": "鎝", - "𨱐": "𨫒", - "𨱑": "鐄", - "𨱒": "鏉", - "𨱓": "鐎", - "𨱔": "鐏", - "𨱕": "𨮂", - "𨱖": "䥩", - "𨷿": "䦳", - "𨸀": "𨳕", - "𨸁": "𨳑", - "𨸂": "閍", - "𨸃": "閐", - "𨸄": "䦘", - "𨸅": "𨴗", - "𨸆": "𨵩", - "𨸇": "𨵸", - "𨸉": "𨶀", - "𨸊": "𨶏", - "𨸋": "𨶲", - "𨸌": "𨶮", - "𨸎": "𨷲", - "𨸘": "𨽏", - "𨸟": "䧢", - "𩏼": "䪏", - "𩏽": "𩏪", - "𩏾": "𩎢", - "𩏿": "䪘", - "𩐀": "䪗", - "𩓋": "顂", - "𩖕": "𩓣", - "𩖖": "顃", - "𩖗": "䫴", - "𩙥": "颰", - "𩙦": "𩗀", - "𩙧": "䬞", - "𩙨": "𩘹", - "𩙩": "𩘀", - "𩙪": "颷", - "𩙫": "颾", - "𩙬": "𩘺", - "𩙭": "𩘝", - "𩙮": "䬘", - "𩙯": "䬝", - "𩙰": "𩙈", - "𩟿": "𩚛", - "𩠀": "𩚥", - "𩠁": "𩚵", - "𩠂": "𩛆", - "𩠃": "𩛩", - "𩠅": "𩟐", - "𩠆": "𩜦", - "𩠇": "䭀", - "𩠈": "䭃", - "𩠉": "𩜇", - "𩠊": "𩜵", - "𩠋": "𩝔", - "𩠌": "餸", - "𩠎": "𩞄", - "𩠏": "𩞦", - "𩠠": "𩠴", - "𩡖": "𩡣", - "𩧦": "𩡺", - "𩧨": "駎", - "𩧩": "𩤊", - "𩧪": "䮾", - "𩧫": "駚", - "𩧬": "𩢡", - "𩧭": "䭿", - "𩧮": "𩢾", - "𩧯": "驋", - "𩧰": "䮝", - "𩧱": "𩥉", - "𩧲": "駧", - "𩧳": "𩢸", - "𩧴": "駩", - "𩧵": "𩢴", - "𩧶": "𩣏", - "𩧸": "𩣫", - "𩧺": "駶", - "𩧻": "𩣵", - "𩧼": "𩣺", - "𩧿": "䮠", - "𩨀": "騔", - "𩨁": "䮞", - "𩨂": "驄", - "𩨃": "騝", - "𩨄": "騪", - "𩨅": "𩤸", - "𩨆": "𩤙", - "𩨇": "䮫", - "𩨈": "騟", - "𩨉": "𩤲", - "𩨊": "騚", - "𩨋": "𩥄", - "𩨌": "𩥑", - "𩨍": "𩥇", - "𩨎": "龭", - "𩨏": "䮳", - "𩨐": "𩧆", - "𩩈": "䯤", - "𩬣": "𩭙", - "𩬤": "𩰀", - "𩭹": "鬖", - "𩯒": "𩯳", - "𩰰": "𩰹", - "𩲒": "𩳤", - "𩴌": "𩴵", - "𩽹": "魥", - "𩽺": "𩵩", - "𩽻": "𩵹", - "𩽼": "鯶", - "𩽽": "𩶱", - "𩽾": "鮟", - "𩽿": "𩶰", - "𩾁": "鯄", - "𩾂": "䲖", - "𩾃": "鮸", - "𩾄": "𩷰", - "𩾅": "𩸃", - "𩾆": "𩸦", - "𩾇": "鯱", - "𩾈": "䱙", - "𩾊": "䱬", - "𩾋": "䱰", - "𩾌": "鱇", - "𩾎": "𩽇", - "𪉂": "䲰", - "𪉃": "鳼", - "𪉄": "𩿪", - "𪉅": "𪀦", - "𪉆": "鴲", - "𪉈": "鴜", - "𪉉": "𪁈", - "𪉊": "鷨", - "𪉋": "𪀾", - "𪉌": "𪁖", - "𪉍": "鵚", - "𪉎": "𪂆", - "𪉏": "𪃏", - "𪉐": "𪃍", - "𪉑": "鷔", - "𪉒": "𪄕", - "𪉔": "𪄆", - "𪉕": "𪇳", - "𪎈": "䴬", - "𪎉": "麲", - "𪎊": "麨", - "𪎋": "䴴", - "𪎌": "麳", - "𪑅": "䵳", - "𪔭": "𪔵", - "𪚏": "𪘀", - "𪚐": "𪘯", - "𪜎": "𠿕", - "𪞝": "凙", - "𪟎": "㔋", - "𪟝": "勣", - "𪠀": "𧷎", - "𪠟": "㓄", - "𪠡": "𠬙", - "𪠳": "唓", - "𪠵": "㖮", - "𪠸": "嚛", - "𪠺": "𠽃", - "𪠽": "噹", - "𪡀": "嘺", - "𪡃": "嘪", - "𪡋": "噞", - "𪡏": "嗹", - "𪡛": "㗿", - "𪡞": "嘳", - "𪡺": "𡃄", - "𪢌": "㘓", - "𪢐": "𡃤", - "𪢒": "𡂡", - "𪢕": "嚽", - "𪢖": "𡅯", - "𪢠": "囒", - "𪢮": "圞", - "𪢸": "墲", - "𪣆": "埬", - "𪣒": "堚", - "𪣻": "塿", - "𪤄": "𡓁", - "𪤚": "壣", - "𪥠": "𧹈", - "𪥫": "孇", - "𪥰": "嬣", - "𪥿": "嬻", - "𪧀": "孾", - "𪧘": "寠", - "𪨊": "㞞", - "𪨗": "屩", - "𪨧": "崙", - "𪨩": "𡸗", - "𪨶": "輋", - "𪨷": "巗", - "𪨹": "𡹬", - "𪩇": "㟺", - "𪩎": "巊", - "𪩘": "巘", - "𪩛": "𡿖", - "𪩷": "幝", - "𪩸": "幩", - "𪪏": "廬", - "𪪑": "㢗", - "𪪞": "廧", - "𪪴": "𢍰", - "𪪼": "彃", - "𪫌": "徿", - "𪫡": "𢤩", - "𪫷": "㦞", - "𪫺": "憸", - "𪬚": "𢣐", - "𪬯": "𢤿", - "𪭝": "𢯷", - "𪭢": "摐", - "𪭧": "擟", - "𪭯": "𢶒", - "𪭵": "掚", - "𪭾": "撊", - "𪮃": "㨻", - "𪮋": "㩋", - "𪮖": "撧", - "𪮳": "𢺳", - "𪮶": "攋", - "𪯋": "㪎", - "𪰶": "曊", - "𪱥": "膹", - "𪱷": "梖", - "𪲎": "櫅", - "𪲔": "欐", - "𪲛": "檵", - "𪲮": "櫠", - "𪳍": "欇", - "𪳗": "𣜬", - "𪴙": "欑", - "𪵑": "毊", - "𪵣": "霼", - "𪵱": "濿", - "𪶄": "溡", - "𪶒": "𤄷", - "𪶮": "𣽏", - "𪷍": "㵾", - "𪷽": "灒", - "𪸕": "熂", - "𪸩": "煇", - "𪹀": "𤑹", - "𪹠": "𤓌", - "𪹳": "爥", - "𪹹": "𤒻", - "𪺣": "𤘀", - "𪺪": "𤜆", - "𪺭": "犞", - "𪺷": "獊", - "𪺸": "𤠮", - "𪺻": "㺜", - "𪺽": "猌", - "𪻐": "瑽", - "𪻨": "瓄", - "𪻲": "瑻", - "𪻺": "璝", - "𪼋": "㻶", - "𪼴": "𤬅", - "𪽈": "畼", - "𪽝": "𤳷", - "𪽪": "痮", - "𪽭": "𤷃", - "𪽮": "㿖", - "𪽴": "𤺔", - "𪽷": "瘱", - "𪾔": "盨", - "𪾢": "睍", - "𪾣": "眝", - "𪾦": "矑", - "𪾸": "矉", - "𪿊": "𥏝", - "𪿞": "𥖲", - "𪿫": "礮", - "𪿵": "𥗇", - "𫀌": "𥜰", - "𫀓": "𥜐", - "𫀨": "䅐", - "𫀬": "䅳", - "𫀮": "𥢷", - "𫁂": "䆉", - "𫁟": "竱", - "𫁡": "鴗", - "𫁱": "𥶽", - "𫁲": "䉑", - "𫁳": "𥯤", - "𫁷": "䉶", - "𫁺": "𥴼", - "𫂃": "簢", - "𫂆": "簂", - "𫂈": "䉬", - "𫂖": "𥴨", - "𫂿": "𥻦", - "𫃗": "𩏷", - "𫄙": "糺", - "𫄚": "䊺", - "𫄛": "紟", - "𫄜": "䋃", - "𫄝": "𥾯", - "𫄞": "䋔", - "𫄟": "絁", - "𫄠": "絙", - "𫄡": "絧", - "𫄢": "絥", - "𫄣": "繷", - "𫄤": "繨", - "𫄥": "纚", - "𫄦": "𦀖", - "𫄧": "綖", - "𫄨": "絺", - "𫄩": "䋦", - "𫄪": "𦅇", - "𫄫": "綟", - "𫄬": "緤", - "𫄭": "緮", - "𫄮": "䋼", - "𫄯": "𦃩", - "𫄰": "縍", - "𫄱": "繬", - "𫄲": "縸", - "𫄳": "縰", - "𫄴": "繂", - "𫄵": "𦅈", - "𫄶": "繈", - "𫄷": "繶", - "𫄸": "纁", - "𫄹": "纗", - "𫅅": "䍤", - "𫅗": "羵", - "𫅥": "𦒀", - "𫅭": "䎙", - "𫅼": "𦔖", - "𫆏": "聻", - "𫆝": "𦟼", - "𫆫": "𦡝", - "𫇘": "𦧺", - "𫇛": "艣", - "𫇪": "𦱌", - "𫇭": "蔿", - "𫇴": "蒭", - "𫇽": "蕽", - "𫈉": "蕳", - "𫈎": "葝", - "𫈟": "蔯", - "𫈵": "蕝", - "𫉁": "薆", - "𫉄": "藷", - "𫊪": "䗅", - "𫊮": "蠦", - "𫊸": "蟜", - "𫊹": "𧒯", - "𫊻": "蟳", - "𫋇": "蟂", - "𫋌": "蟘", - "𫋲": "䙔", - "𫋷": "襗", - "𫋹": "襓", - "𫋻": "襘", - "𫌀": "襀", - "𫌇": "襵", - "𫌋": "𧞫", - "𫌨": "覼", - "𫌪": "覛", - "𫌫": "𧡴", - "𫌬": "𧢄", - "𫌭": "覹", - "𫌯": "䚩", - "𫍐": "𧭹", - "𫍙": "訑", - "𫍚": "訞", - "𫍛": "訜", - "𫍜": "詓", - "𫍝": "諫", - "𫍞": "𧦝", - "𫍟": "𧦧", - "𫍠": "䛄", - "𫍡": "詑", - "𫍢": "譊", - "𫍣": "詷", - "𫍤": "譑", - "𫍥": "誂", - "𫍦": "譨", - "𫍧": "誺", - "𫍨": "誫", - "𫍩": "諣", - "𫍪": "誋", - "𫍫": "䛳", - "𫍬": "誷", - "𫍭": "𧩕", - "𫍮": "誳", - "𫍯": "諴", - "𫍰": "諰", - "𫍱": "諯", - "𫍲": "謏", - "𫍳": "諥", - "𫍴": "謱", - "𫍵": "謸", - "𫍶": "𧩼", - "𫍷": "謉", - "𫍸": "謆", - "𫍹": "謯", - "𫍺": "𧫝", - "𫍻": "譆", - "𫍼": "𧬤", - "𫍽": "譞", - "𫍾": "𧭈", - "𫍿": "譾", - "𫎆": "豵", - "𫎌": "貗", - "𫎦": "贚", - "𫎧": "䝭", - "𫎨": "𧸘", - "𫎩": "賝", - "𫎪": "䞋", - "𫎫": "贉", - "𫎬": "贑", - "𫎭": "䞓", - "𫎱": "䟐", - "𫎳": "䟆", - "𫎸": "𧽯", - "𫎺": "䟃", - "𫏃": "䠆", - "𫏆": "蹳", - "𫏋": "蹻", - "𫏌": "𨂐", - "𫏐": "蹔", - "𫏑": "𨇽", - "𫏕": "𨆪", - "𫏞": "𨇰", - "𫏨": "𨇤", - "𫐄": "軏", - "𫐅": "軕", - "𫐆": "轣", - "𫐇": "軜", - "𫐈": "軷", - "𫐉": "軨", - "𫐊": "軬", - "𫐋": "𨎌", - "𫐌": "軿", - "𫐍": "𨌈", - "𫐎": "輢", - "𫐏": "輖", - "𫐐": "輗", - "𫐑": "輨", - "𫐒": "輷", - "𫐓": "輮", - "𫐔": "𨍰", - "𫐕": "轊", - "𫐖": "轇", - "𫐗": "轐", - "𫐘": "轗", - "𫐙": "轠", - "𫐷": "遱", - "𫑘": "鄟", - "𫑡": "鄳", - "𫑷": "醶", - "𫓥": "釟", - "𫓦": "釨", - "𫓧": "鈇", - "𫓨": "鈛", - "𫓩": "鏦", - "𫓪": "鈆", - "𫓫": "𨥟", - "𫓬": "鉔", - "𫓭": "鉠", - "𫓮": "𨪕", - "𫓯": "銈", - "𫓰": "銊", - "𫓱": "鐈", - "𫓲": "銁", - "𫓳": "𨰋", - "𫓴": "鉾", - "𫓵": "鋠", - "𫓶": "鋗", - "𫓷": "𫒡", - "𫓸": "錽", - "𫓹": "錤", - "𫓺": "鐪", - "𫓻": "錜", - "𫓼": "𨨛", - "𫓽": "錝", - "𫓾": "錥", - "𫓿": "𨨢", - "𫔀": "鍊", - "𫔁": "鐼", - "𫔂": "鍉", - "𫔃": "𨰲", - "𫔄": "鍒", - "𫔅": "鎍", - "𫔆": "䥯", - "𫔇": "鎞", - "𫔈": "鎙", - "𫔉": "𨰃", - "𫔊": "鏥", - "𫔋": "䥗", - "𫔌": "鏾", - "𫔍": "鐇", - "𫔎": "鐍", - "𫔏": "𨬖", - "𫔐": "𨭸", - "𫔑": "𨭖", - "𫔒": "𨮳", - "𫔓": "𨯟", - "𫔔": "鑴", - "𫔕": "𨰥", - "𫔖": "𨲳", - "𫔭": "開", - "𫔮": "閒", - "𫔯": "閗", - "𫔰": "閞", - "𫔲": "𨴹", - "𫔴": "閵", - "𫔵": "䦯", - "𫔶": "闑", - "𫔽": "𨼳", - "𫕚": "𩀨", - "𫕥": "霣", - "𫕨": "𩅙", - "𫖃": "靧", - "𫖅": "䪊", - "𫖇": "鞾", - "𫖑": "𩎖", - "𫖒": "韠", - "𫖓": "𩏂", - "𫖔": "韛", - "𫖕": "韝", - "𫖖": "𩏠", - "𫖪": "𩑔", - "𫖫": "䪴", - "𫖬": "䪾", - "𫖭": "𩒎", - "𫖮": "顗", - "𫖯": "頫", - "𫖰": "䫂", - "𫖱": "䫀", - "𫖲": "䫟", - "𫖳": "頵", - "𫖴": "𩔳", - "𫖵": "𩓥", - "𫖶": "顅", - "𫖷": "𩔑", - "𫖸": "願", - "𫖹": "顣", - "𫖺": "䫶", - "𫗇": "䫻", - "𫗈": "𩗓", - "𫗉": "𩗴", - "𫗊": "䬓", - "𫗋": "飋", - "𫗚": "𩟗", - "𫗞": "飦", - "𫗟": "䬧", - "𫗠": "餦", - "𫗡": "𩚩", - "𫗢": "飵", - "𫗣": "飶", - "𫗤": "𩛌", - "𫗥": "餫", - "𫗦": "餔", - "𫗧": "餗", - "𫗨": "𩛡", - "𫗩": "饠", - "𫗪": "餧", - "𫗫": "餬", - "𫗬": "餪", - "𫗭": "餵", - "𫗮": "餭", - "𫗯": "餱", - "𫗰": "䭔", - "𫗱": "䭑", - "𫗳": "𩝽", - "𫗴": "饘", - "𫗵": "饟", - "𫘛": "馯", - "𫘜": "馼", - "𫘝": "駃", - "𫘞": "駞", - "𫘟": "駊", - "𫘠": "駤", - "𫘡": "駫", - "𫘣": "駻", - "𫘤": "騃", - "𫘥": "騉", - "𫘦": "騊", - "𫘧": "騄", - "𫘨": "騠", - "𫘩": "騜", - "𫘪": "騵", - "𫘫": "騴", - "𫘬": "騱", - "𫘭": "騻", - "𫘮": "䮰", - "𫘯": "驓", - "𫘰": "驙", - "𫘱": "驨", - "𫘽": "鬠", - "𫙂": "𩯁", - "𫚈": "鱮", - "𫚉": "魟", - "𫚊": "鰑", - "𫚋": "鱄", - "𫚌": "魦", - "𫚍": "魵", - "𫚎": "𩶁", - "𫚏": "䱁", - "𫚐": "䱀", - "𫚑": "鮅", - "𫚒": "鮄", - "𫚓": "鮤", - "𫚔": "鮰", - "𫚕": "鰤", - "𫚖": "鮆", - "𫚗": "鮯", - "𫚘": "𩻮", - "𫚙": "鯆", - "𫚚": "鮿", - "𫚛": "鮵", - "𫚜": "䲅", - "𫚝": "𩸄", - "𫚞": "鯬", - "𫚟": "𩸡", - "𫚠": "䱧", - "𫚡": "鯞", - "𫚢": "鰋", - "𫚣": "鯾", - "𫚤": "鰦", - "𫚥": "鰕", - "𫚦": "鰫", - "𫚧": "鰽", - "𫚨": "𩻗", - "𫚩": "𩻬", - "𫚪": "鱊", - "𫚫": "鱢", - "𫚬": "𩼶", - "𫚭": "鱲", - "𫛚": "鳽", - "𫛛": "鳷", - "𫛜": "鴀", - "𫛝": "鴅", - "𫛞": "鴃", - "𫛟": "鸗", - "𫛠": "𩿤", - "𫛡": "鴔", - "𫛢": "鸋", - "𫛣": "鴥", - "𫛤": "鴐", - "𫛥": "鵊", - "𫛦": "鴮", - "𫛧": "𪀖", - "𫛨": "鵧", - "𫛩": "鴳", - "𫛪": "鴽", - "𫛫": "鶰", - "𫛬": "䳜", - "𫛭": "鵟", - "𫛮": "䳤", - "𫛯": "鶭", - "𫛰": "䳢", - "𫛱": "鵫", - "𫛲": "鵰", - "𫛳": "鵩", - "𫛴": "鷤", - "𫛵": "鶌", - "𫛶": "鶒", - "𫛷": "鶦", - "𫛸": "鶗", - "𫛹": "𪃧", - "𫛺": "䳧", - "𫛻": "𪃒", - "𫛼": "䳫", - "𫛽": "鷅", - "𫛾": "𪆷", - "𫜀": "鷐", - "𫜁": "鷩", - "𫜂": "𪅂", - "𫜃": "鷣", - "𫜄": "鷷", - "𫜅": "䴋", - "𫜊": "𪉸", - "𫜑": "麷", - "𫜒": "䴱", - "𫜓": "𪌭", - "𫜔": "䴽", - "𫜕": "𪍠", - "𫜙": "䵴", - "𫜟": "𪓰", - "𫜨": "𪶕", - "𫜩": "齧", - "𫜪": "齩", - "𫜫": "𫜦", - "𫜬": "齰", - "𫜭": "齭", - "𫜮": "齴", - "𫜯": "𪙏", - "𫜰": "齾", - "𫜲": "龓", - "𫜳": "䶲", - "𫝈": "㑮", - "𫝋": "𠐊", - "𫝦": "㛝", - "𫝧": "㜐", - "𫝨": "媈", - "𫝩": "嬦", - "𫝪": "𡟫", - "𫝫": "婡", - "𫝬": "嬇", - "𫝭": "孆", - "𫝮": "孄", - "𫝵": "嶹", - "𫞅": "𦠅", - "𫞗": "潣", - "𫞚": "澬", - "𫞛": "㶆", - "𫞝": "灍", - "𫞠": "爧", - "𫞡": "爃", - "𫞢": "𤛱", - "𫞣": "㹽", - "𫞥": "珼", - "𫞦": "璾", - "𫞧": "𤩂", - "𫞨": "璼", - "𫞩": "璊", - "𫞷": "𥢶", - "𫟃": "絍", - "𫟄": "綋", - "𫟅": "綡", - "𫟆": "緟", - "𫟇": "𦆲", - "𫟑": "䖅", - "𫟕": "䕤", - "𫟞": "訨", - "𫟟": "詊", - "𫟠": "譂", - "𫟡": "誴", - "𫟢": "䜖", - "𫟤": "䡐", - "𫟥": "䡩", - "𫟦": "䡵", - "𫟫": "𨞺", - "𫟬": "𨟊", - "𫟲": "釚", - "𫟳": "釲", - "𫟴": "鈖", - "𫟵": "鈗", - "𫟶": "銏", - "𫟷": "鉝", - "𫟸": "鉽", - "𫟹": "鉷", - "𫟺": "䤤", - "𫟻": "銂", - "𫟼": "鐽", - "𫟽": "𨧰", - "𫟾": "𨩰", - "𫟿": "鎈", - "𫠀": "䥄", - "𫠁": "鑉", - "𫠂": "閝", - "𫠅": "韚", - "𫠆": "頍", - "𫠇": "𩖰", - "𫠈": "䫾", - "𫠊": "䮄", - "𫠋": "騼", - "𫠌": "𩦠", - "𫠏": "𩵦", - "𫠐": "魽", - "𫠑": "䱸", - "𫠒": "鱆", - "𫠖": "𩿅", - "𫠜": "齯", - "𫢸": "僤", - "𫧃": "𣍐", - "𫧮": "𪋿", - "𫫇": "噁", - "𫬐": "㘔", - "𫭟": "塸", - "𫭢": "埨", - "𫭼": "𡑍", - "𫮃": "墠", - "𫰛": "娙", - "𫵷": "㠣", - "𫶇": "嵽", - "𫷷": "廞", - "𫸩": "彄", - "𬀩": "暐", - "𬀪": "晛", - "𬂩": "梜", - "𬃊": "櫍", - "𬇕": "澫", - "𬇙": "浿", - "𬇹": "漍", - "𬉼": "熰", - "𬊈": "燖", - "𬊤": "燀", - "𬍛": "瓅", - "𬍡": "璗", - "𬍤": "璕", - "𬒈": "礐", - "𬒗": "𥗽", - "𬕂": "篢", - "𬘓": "紃", - "𬘘": "紞", - "𬘡": "絪", - "𬘩": "綎", - "𬘫": "綄", - "𬘬": "綪", - "𬘭": "綝", - "𬘯": "綧", - "𬙂": "縯", - "𬙊": "纆", - "𬙋": "纕", - "𬜬": "蔄", - "𬜯": "䓣", - "𬞟": "蘋", - "𬟁": "虉", - "𬟽": "蝀", - "𬣙": "訏", - "𬣞": "詝", - "𬣡": "諓", - "𬣳": "詪", - "𬤇": "諲", - "𬤊": "諟", - "𬤝": "譓", - "𬨂": "軝", - "𬨎": "輶", - "𬩽": "鄩", - "𬪩": "醲", - "𬬩": "釴", - "𬬭": "錀", - "𬬮": "鋹", - "𬬱": "釿", - "𬬸": "鉥", - "𬬹": "鉮", - "𬬻": "鑪", - "𬬿": "鉊", - "𬭁": "鉧", - "𬭊": "𨧀", - "𬭎": "鋐", - "𬭚": "錞", - "𬭛": "𨨏", - "𬭤": "鍭", - "𬭩": "鎓", - "𬭬": "鏏", - "𬭭": "鏚", - "𬭯": "䥕", - "𬭳": "𨭎", - "𬭶": "𨭆", - "𬭸": "鏻", - "𬭼": "鐩", - "𬮱": "闉", - "𬮿": "隑", - "𬯀": "隮", - "𬯎": "隤", - "𬱖": "頔", - "𬱟": "頠", - "𬳵": "駓", - "𬳶": "駉", - "𬳽": "駪", - "𬳿": "駼", - "𬴂": "騑", - "𬴃": "騞", - "𬴊": "驎", - "𬶋": "鮈", - "𬶍": "鮀", - "𬶏": "鮠", - "𬶐": "鮡", - "𬶟": "鯻", - "𬶠": "鰊", - "𬶨": "鱀", - "𬶭": "鰶", - "𬶮": "鱚", - "𬷕": "鵏", - "𬸘": "鶠", - "𬸚": "鸑", - "𬸣": "鶱", - "𬸦": "鷟", - "𬸪": "鷭", - "𬸯": "鷿", - "𬹼": "齘", - "𬺈": "齮", - "𬺓": "齼", - "𰬸": "繐", - "𰰨": "菕", - "𰶎": "譅", - "𰾄": "鋂", - "𰾭": "鑀", - "𱊜": "𪈼", -}; -const t2sData = { - 㑮: "𫝈", - 㑯: "㑔", - 㑳: "㑇", - 㑶: "㐹", - 㒓: "𠉂", - 㓄: "𪠟", - 㓨: "刾", - 㔋: "𪟎", - 㖮: "𪠵", - 㗲: "𠵾", - 㗿: "𪡛", - 㘉: "𠰱", - 㘓: "𪢌", - 㘔: "𫬐", - 㘚: "㘎", - 㛝: "𫝦", - 㜄: "㚯", - 㜏: "㛣", - 㜐: "𫝧", - 㜗: "𡞋", - 㜢: "𡞱", - 㜷: "𡝠", - 㞞: "𪨊", - 㟺: "𪩇", - 㠏: "㟆", - 㠣: "𫵷", - 㢗: "𪪑", - 㢝: "𢋈", - 㥮: "㤘", - 㦎: "𢛯", - 㦛: "𢗓", - 㦞: "𪫷", - 㨻: "𪮃", - 㩋: "𪮋", - 㩜: "㨫", - 㩳: "㧐", - 㩵: "擜", - 㪎: "𪯋", - 㯤: "𣘐", - 㰙: "𣗙", - 㵗: "𣳆", - 㵾: "𪷍", - 㶆: "𫞛", - 㷍: "𤆢", - 㷿: "𤈷", - 㸇: "𤎺", - 㹽: "𫞣", - 㺏: "𤠋", - 㺜: "𪺻", - 㻶: "𪼋", - 㿖: "𪽮", - 㿗: "𤻊", - 㿧: "𤽯", - 䀉: "𥁢", - 䀹: "𥅴", - 䁪: "𥇢", - 䁻: "䀥", - 䂎: "𥎝", - 䃮: "鿎", - 䅐: "𫀨", - 䅳: "𫀬", - 䆉: "𫁂", - 䉑: "𫁲", - 䉙: "𥬀", - 䉬: "𫂈", - 䉲: "𥮜", - 䉶: "𫁷", - 䊭: "𥺅", - 䊷: "䌶", - 䊺: "𫄚", - 䋃: "𫄜", - 䋔: "𫄞", - 䋙: "䌺", - 䋚: "䌻", - 䋦: "𫄩", - 䋹: "䌿", - 䋻: "䌾", - 䋼: "𫄮", - 䋿: "𦈓", - 䌈: "𦈖", - 䌋: "𦈘", - 䌖: "𦈜", - 䌝: "𦈟", - 䌟: "𦈞", - 䌥: "𦈠", - 䌰: "𦈙", - 䍤: "𫅅", - 䍦: "䍠", - 䍽: "𦍠", - 䎙: "𫅭", - 䎱: "䎬", - 䓣: "𬜯", - 䕤: "𫟕", - 䕳: "𦰴", - 䖅: "𫟑", - 䗅: "𫊪", - 䗿: "𧉞", - 䙔: "𫋲", - 䙡: "䙌", - 䙱: "𧜭", - 䚩: "𫌯", - 䛄: "𫍠", - 䛳: "𫍫", - 䜀: "䜧", - 䜖: "𫟢", - 䝭: "𫎧", - 䝻: "𧹕", - 䝼: "䞍", - 䞈: "𧹑", - 䞋: "𫎪", - 䞓: "𫎭", - 䟃: "𫎺", - 䟆: "𫎳", - 䟐: "𫎱", - 䠆: "𫏃", - 䠱: "𨅛", - 䡐: "𫟤", - 䡩: "𫟥", - 䡵: "𫟦", - 䢨: "𨑹", - 䤤: "𫟺", - 䥄: "𫠀", - 䥇: "䦂", - 䥑: "鿏", - 䥕: "𬭯", - 䥗: "𫔋", - 䥩: "𨱖", - 䥯: "𫔆", - 䥱: "䥾", - 䦘: "𨸄", - 䦛: "䦶", - 䦟: "䦷", - 䦯: "𫔵", - 䦳: "𨷿", - 䧢: "𨸟", - 䪊: "𫖅", - 䪏: "𩏼", - 䪗: "𩐀", - 䪘: "𩏿", - 䪴: "𫖫", - 䪾: "𫖬", - 䫀: "𫖱", - 䫂: "𫖰", - 䫟: "𫖲", - 䫴: "𩖗", - 䫶: "𫖺", - 䫻: "𫗇", - 䫾: "𫠈", - 䬓: "𫗊", - 䬘: "𩙮", - 䬝: "𩙯", - 䬞: "𩙧", - 䬧: "𫗟", - 䭀: "𩠇", - 䭃: "𩠈", - 䭑: "𫗱", - 䭔: "𫗰", - 䭿: "𩧭", - 䮄: "𫠊", - 䮝: "𩧰", - 䮞: "𩨁", - 䮠: "𩧿", - 䮫: "𩨇", - 䮰: "𫘮", - 䮳: "𩨏", - 䮾: "𩧪", - 䯀: "䯅", - 䯤: "𩩈", - 䰾: "鲃", - 䱀: "𫚐", - 䱁: "𫚏", - 䱙: "𩾈", - 䱧: "𫚠", - 䱬: "𩾊", - 䱰: "𩾋", - 䱷: "䲣", - 䱸: "𫠑", - 䱽: "䲝", - 䲁: "鳚", - 䲅: "𫚜", - 䲖: "𩾂", - 䲘: "鳤", - 䲰: "𪉂", - 䳜: "𫛬", - 䳢: "𫛰", - 䳤: "𫛮", - 䳧: "𫛺", - 䳫: "𫛼", - 䴉: "鹮", - 䴋: "𫜅", - 䴬: "𪎈", - 䴱: "𫜒", - 䴴: "𪎋", - 䴽: "𫜔", - 䵳: "𪑅", - 䵴: "𫜙", - 䶕: "𫜨", - 䶲: "𫜳", - 丟: "丢", - 並: "并", - 乾: "干", - 亂: "乱", - 亙: "亘", - 亞: "亚", - 佇: "伫", - 佈: "布", - 佔: "占", - 併: "并", - 來: "来", - 侖: "仑", - 侶: "侣", - 侷: "局", - 俁: "俣", - 係: "系", - 俓: "𠇹", - 俔: "伣", - 俠: "侠", - 俥: "伡", - 俬: "私", - 倀: "伥", - 倆: "俩", - 倈: "俫", - 倉: "仓", - 個: "个", - 們: "们", - 倖: "幸", - 倫: "伦", - 倲: "㑈", - 偉: "伟", - 偑: "㐽", - 側: "侧", - 偵: "侦", - 偽: "伪", - 傌: "㐷", - 傑: "杰", - 傖: "伧", - 傘: "伞", - 備: "备", - 傢: "家", - 傭: "佣", - 傯: "偬", - 傳: "传", - 傴: "伛", - 債: "债", - 傷: "伤", - 傾: "倾", - 僂: "偻", - 僅: "仅", - 僉: "佥", - 僑: "侨", - 僕: "仆", - 僞: "伪", - 僤: "𫢸", - 僥: "侥", - 僨: "偾", - 僱: "雇", - 價: "价", - 儀: "仪", - 儁: "俊", - 儂: "侬", - 億: "亿", - 儈: "侩", - 儉: "俭", - 儎: "", - 儐: "", - 儔: "俦", - 儕: "侪", - 儘: "尽", - 償: "偿", - 儣: "𠆲", - 優: "优", - 儭: "𠋆", - 儲: "储", - 儷: "俪", - 儸: "㑩", - 儺: "傩", - 儻: "傥", - 儼: "俨", - 兇: "凶", - 兌: "兑", - 兒: "儿", - 兗: "兖", - 內: "内", - 兩: "两", - 冊: "册", - 冑: "胄", - 冪: "幂", - 凈: "净", - 凍: "冻", - 凙: "𪞝", - 凜: "凛", - 凱: "凯", - 別: "别", - 刪: "删", - 剄: "刭", - 則: "则", - 剋: "克", - 剎: "刹", - 剗: "刬", - 剛: "刚", - 剝: "剥", - 剮: "剐", - 剴: "剀", - 創: "创", - 剷: "铲", - 剾: "𠛅", - 劃: "划", - 劇: "剧", - 劉: "刘", - 劊: "刽", - 劌: "刿", - 劍: "剑", - 劏: "㓥", - 劑: "剂", - 劚: "㔉", - 勁: "劲", - 勑: "𠡠", - 動: "动", - 務: "务", - 勛: "勋", - 勝: "胜", - 勞: "劳", - 勢: "势", - 勣: "𪟝", - 勩: "勚", - 勱: "劢", - 勳: "勋", - 勵: "励", - 勸: "劝", - 勻: "匀", - 匭: "匦", - 匯: "汇", - 匱: "匮", - 區: "区", - 協: "协", - 卹: "恤", - 卻: "却", - 卽: "即", - 厙: "厍", - 厠: "厕", - 厤: "历", - 厭: "厌", - 厲: "厉", - 厴: "厣", - 參: "参", - 叄: "叁", - 叢: "丛", - 吒: "咤", - 吳: "吴", - 吶: "呐", - 呂: "吕", - 咼: "呙", - 員: "员", - 哯: "𠯟", - 唄: "呗", - 唓: "𪠳", - 唸: "念", - 問: "问", - 啓: "启", - 啞: "哑", - 啟: "启", - 啢: "唡", - 喎: "㖞", - 喚: "唤", - 喪: "丧", - 喫: "吃", - 喬: "乔", - 單: "单", - 喲: "哟", - 嗆: "呛", - 嗇: "啬", - 嗊: "唝", - 嗎: "吗", - 嗚: "呜", - 嗩: "唢", - 嗰: "𠮶", - 嗶: "哔", - 嗹: "𪡏", - 嘆: "叹", - 嘍: "喽", - 嘓: "啯", - 嘔: "呕", - 嘖: "啧", - 嘗: "尝", - 嘜: "唛", - 嘩: "哗", - 嘪: "𪡃", - 嘮: "唠", - 嘯: "啸", - 嘰: "叽", - 嘳: "𪡞", - 嘵: "哓", - 嘸: "呒", - 嘺: "𪡀", - 嘽: "啴", - 噁: "恶", - 噅: "𠯠", - 噓: "嘘", - 噚: "㖊", - 噝: "咝", - 噞: "𪡋", - 噠: "哒", - 噥: "哝", - 噦: "哕", - 噯: "嗳", - 噲: "哙", - 噴: "喷", - 噸: "吨", - 噹: "当", - 嚀: "咛", - 嚇: "吓", - 嚌: "哜", - 嚐: "尝", - 嚕: "噜", - 嚙: "啮", - 嚛: "𪠸", - 嚥: "咽", - 嚦: "呖", - 嚧: "𠰷", - 嚨: "咙", - 嚮: "向", - 嚲: "亸", - 嚳: "喾", - 嚴: "严", - 嚶: "嘤", - 嚽: "𪢕", - 囀: "啭", - 囁: "嗫", - 囂: "嚣", - 囃: "𠱞", - 囅: "冁", - 囈: "呓", - 囉: "啰", - 囌: "苏", - 囑: "嘱", - 囒: "𪢠", - 囪: "囱", - 圇: "囵", - 國: "国", - 圍: "围", - 園: "园", - 圓: "圆", - 圖: "图", - 團: "团", - 圞: "𪢮", - 垻: "坝", - 埡: "垭", - 埨: "𫭢", - 埬: "𪣆", - 埰: "采", - 執: "执", - 堅: "坚", - 堊: "垩", - 堖: "垴", - 堚: "𪣒", - 堝: "埚", - 堯: "尧", - 報: "报", - 場: "场", - 塊: "块", - 塋: "茔", - 塏: "垲", - 塒: "埘", - 塗: "涂", - 塚: "冢", - 塢: "坞", - 塤: "埙", - 塵: "尘", - 塸: "𫭟", - 塹: "堑", - 塿: "𪣻", - 墊: "垫", - 墜: "坠", - 墠: "𫮃", - 墮: "堕", - 墰: "坛", - 墲: "𪢸", - 墳: "坟", - 墶: "垯", - 墻: "墙", - 墾: "垦", - 壇: "坛", - 壈: "𡒄", - 壋: "垱", - 壎: "埙", - 壓: "压", - 壗: "𡋤", - 壘: "垒", - 壙: "圹", - 壚: "垆", - 壜: "坛", - 壞: "坏", - 壟: "垄", - 壠: "垅", - 壢: "坜", - 壣: "𪤚", - 壩: "坝", - 壪: "塆", - 壯: "壮", - 壺: "壶", - 壼: "壸", - 壽: "寿", - 夠: "够", - 夢: "梦", - 夥: "伙", - 夾: "夹", - 奐: "奂", - 奧: "奥", - 奩: "奁", - 奪: "夺", - 奬: "奖", - 奮: "奋", - 奼: "姹", - 妝: "妆", - 姍: "姗", - 姦: "奸", - 娙: "𫰛", - 娛: "娱", - 婁: "娄", - 婡: "𫝫", - 婦: "妇", - 婭: "娅", - 媈: "𫝨", - 媧: "娲", - 媯: "妫", - 媰: "㛀", - 媼: "媪", - 媽: "妈", - 嫋: "袅", - 嫗: "妪", - 嫵: "妩", - 嫺: "娴", - 嫻: "娴", - 嫿: "婳", - 嬀: "妫", - 嬃: "媭", - 嬇: "𫝬", - 嬈: "娆", - 嬋: "婵", - 嬌: "娇", - 嬙: "嫱", - 嬡: "嫒", - 嬣: "𪥰", - 嬤: "嬷", - 嬦: "𫝩", - 嬪: "嫔", - 嬰: "婴", - 嬸: "婶", - 嬻: "𪥿", - 孃: "娘", - 孄: "𫝮", - 孆: "𫝭", - 孇: "𪥫", - 孋: "㛤", - 孌: "娈", - 孎: "𡠟", - 孫: "孙", - 學: "学", - 孻: "𡥧", - 孾: "𪧀", - 孿: "孪", - 宮: "宫", - 寀: "采", - 寠: "𪧘", - 寢: "寝", - 實: "实", - 寧: "宁", - 審: "审", - 寫: "写", - 寬: "宽", - 寵: "宠", - 寶: "宝", - 將: "将", - 專: "专", - 尋: "寻", - 對: "对", - 導: "导", - 尷: "尴", - 屆: "届", - 屍: "尸", - 屓: "屃", - 屜: "屉", - 屢: "屡", - 層: "层", - 屨: "屦", - 屩: "𪨗", - 屬: "属", - 岡: "冈", - 峯: "峰", - 峴: "岘", - 島: "岛", - 峽: "峡", - 崍: "崃", - 崑: "昆", - 崗: "岗", - 崙: "仑", - 崢: "峥", - 崬: "岽", - 嵐: "岚", - 嵗: "岁", - 嵼: "𡶴", - 嵽: "𫶇", - 嵾: "㟥", - 嶁: "嵝", - 嶄: "崭", - 嶇: "岖", - 嶈: "𡺃", - 嶔: "嵚", - 嶗: "崂", - 嶘: "𡺄", - 嶠: "峤", - 嶢: "峣", - 嶧: "峄", - 嶨: "峃", - 嶮: "崄", - 嶸: "嵘", - 嶹: "𫝵", - 嶺: "岭", - 嶼: "屿", - 嶽: "岳", - 巊: "𪩎", - 巋: "岿", - 巒: "峦", - 巔: "巅", - 巖: "岩", - 巗: "𪨷", - 巘: "𪩘", - 巰: "巯", - 巹: "卺", - 帥: "帅", - 師: "师", - 帳: "帐", - 帶: "带", - 幀: "帧", - 幃: "帏", - 幓: "㡎", - 幗: "帼", - 幘: "帻", - 幝: "𪩷", - 幟: "帜", - 幣: "币", - 幩: "𪩸", - 幫: "帮", - 幬: "帱", - 幹: "干", - 幾: "几", - 庫: "库", - 廁: "厕", - 廂: "厢", - 廄: "厩", - 廈: "厦", - 廎: "庼", - 廕: "荫", - 廚: "厨", - 廝: "厮", - 廞: "𫷷", - 廟: "庙", - 廠: "厂", - 廡: "庑", - 廢: "废", - 廣: "广", - 廧: "𪪞", - 廩: "廪", - 廬: "庐", - 廳: "厅", - 弒: "弑", - 弔: "吊", - 弳: "弪", - 張: "张", - 強: "强", - 彃: "𪪼", - 彄: "𫸩", - 彆: "别", - 彈: "弹", - 彌: "弥", - 彎: "弯", - 彔: "录", - 彙: "汇", - 彠: "彟", - 彥: "彦", - 彫: "雕", - 彲: "彨", - 彷: "彷", - 彿: "佛", - 後: "后", - 徑: "径", - 從: "从", - 徠: "徕", - 復: "复", - 徵: "征", - 徹: "彻", - 徿: "𪫌", - 恆: "恒", - 恥: "耻", - 悅: "悦", - 悞: "悮", - 悵: "怅", - 悶: "闷", - 悽: "凄", - 惡: "恶", - 惱: "恼", - 惲: "恽", - 惻: "恻", - 愛: "爱", - 愜: "惬", - 愨: "悫", - 愴: "怆", - 愷: "恺", - 愻: "𢙏", - 愾: "忾", - 慄: "栗", - 態: "态", - 慍: "愠", - 慘: "惨", - 慚: "惭", - 慟: "恸", - 慣: "惯", - 慤: "悫", - 慪: "怄", - 慫: "怂", - 慮: "虑", - 慳: "悭", - 慶: "庆", - 慺: "㥪", - 慼: "戚", - 慾: "欲", - 憂: "忧", - 憊: "惫", - 憐: "怜", - 憑: "凭", - 憒: "愦", - 憖: "慭", - 憚: "惮", - 憢: "𢙒", - 憤: "愤", - 憫: "悯", - 憮: "怃", - 憲: "宪", - 憶: "忆", - 憸: "𪫺", - 憹: "𢙐", - 懀: "𢙓", - 懇: "恳", - 應: "应", - 懌: "怿", - 懍: "懔", - 懎: "𢠁", - 懞: "蒙", - 懟: "怼", - 懣: "懑", - 懤: "㤽", - 懨: "恹", - 懲: "惩", - 懶: "懒", - 懷: "怀", - 懸: "悬", - 懺: "忏", - 懼: "惧", - 懾: "慑", - 戀: "恋", - 戇: "戆", - 戔: "戋", - 戧: "戗", - 戩: "戬", - 戰: "战", - 戱: "戯", - 戲: "戏", - 戶: "户", - 拋: "抛", - 挩: "捝", - 挱: "挲", - 挾: "挟", - 捨: "舍", - 捫: "扪", - 捱: "挨", - 捲: "卷", - 掃: "扫", - 掄: "抡", - 掆: "㧏", - 掗: "挜", - 掙: "挣", - 掚: "𪭵", - 掛: "挂", - 採: "采", - 揀: "拣", - 揚: "扬", - 換: "换", - 揮: "挥", - 揯: "搄", - 損: "损", - 搖: "摇", - 搗: "捣", - 搵: "揾", - 搶: "抢", - 摋: "𢫬", - 摐: "𪭢", - 摑: "掴", - 摜: "掼", - 摟: "搂", - 摯: "挚", - 摳: "抠", - 摶: "抟", - 摺: "折", - 摻: "掺", - 撈: "捞", - 撊: "𪭾", - 撏: "挦", - 撐: "撑", - 撓: "挠", - 撝: "㧑", - 撟: "挢", - 撣: "掸", - 撥: "拨", - 撧: "𪮖", - 撫: "抚", - 撲: "扑", - 撳: "揿", - 撻: "挞", - 撾: "挝", - 撿: "捡", - 擁: "拥", - 擄: "掳", - 擇: "择", - 擊: "击", - 擋: "挡", - 擓: "㧟", - 擔: "担", - 據: "据", - 擟: "𪭧", - 擠: "挤", - 擣: "捣", - 擫: "𢬍", - 擬: "拟", - 擯: "摈", - 擰: "拧", - 擱: "搁", - 擲: "掷", - 擴: "扩", - 擷: "撷", - 擺: "摆", - 擻: "擞", - 擼: "撸", - 擽: "㧰", - 擾: "扰", - 攄: "摅", - 攆: "撵", - 攋: "𪮶", - 攏: "拢", - 攔: "拦", - 攖: "撄", - 攙: "搀", - 攛: "撺", - 攜: "携", - 攝: "摄", - 攢: "攒", - 攣: "挛", - 攤: "摊", - 攪: "搅", - 攬: "揽", - 敎: "教", - 敓: "敚", - 敗: "败", - 敘: "叙", - 敵: "敌", - 數: "数", - 斂: "敛", - 斃: "毙", - 斅: "𢽾", - 斆: "敩", - 斕: "斓", - 斬: "斩", - 斷: "断", - 斸: "𣃁", - 於: "于", - 旂: "旗", - 旣: "既", - 昇: "升", - 時: "时", - 晉: "晋", - 晛: "𬀪", - 晝: "昼", - 暈: "晕", - 暉: "晖", - 暐: "𬀩", - 暘: "旸", - 暢: "畅", - 暫: "暂", - 曄: "晔", - 曆: "历", - 曇: "昙", - 曉: "晓", - 曊: "𪰶", - 曏: "向", - 曖: "暧", - 曠: "旷", - 曥: "𣆐", - 曨: "昽", - 曬: "晒", - 書: "书", - 會: "会", - 朥: "𦛨", - 朧: "胧", - 朮: "术", - 東: "东", - 枴: "拐", - 柵: "栅", - 柺: "拐", - 査: "查", - 桱: "𣐕", - 桿: "杆", - 梔: "栀", - 梖: "𪱷", - 梘: "枧", - 梜: "𬂩", - 條: "条", - 梟: "枭", - 梲: "棁", - 棄: "弃", - 棊: "棋", - 棖: "枨", - 棗: "枣", - 棟: "栋", - 棡: "㭎", - 棧: "栈", - 棲: "栖", - 棶: "梾", - 椏: "桠", - 椲: "㭏", - 楇: "𣒌", - 楊: "杨", - 楓: "枫", - 楨: "桢", - 業: "业", - 極: "极", - 榘: "矩", - 榦: "干", - 榪: "杩", - 榮: "荣", - 榲: "榅", - 榿: "桤", - 構: "构", - 槍: "枪", - 槓: "杠", - 槤: "梿", - 槧: "椠", - 槨: "椁", - 槫: "𣏢", - 槮: "椮", - 槳: "桨", - 槶: "椢", - 槼: "椝", - 樁: "桩", - 樂: "乐", - 樅: "枞", - 樑: "梁", - 樓: "楼", - 標: "标", - 樞: "枢", - 樠: "𣗊", - 樢: "㭤", - 樣: "样", - 樤: "𣔌", - 樧: "榝", - 樫: "㭴", - 樳: "桪", - 樸: "朴", - 樹: "树", - 樺: "桦", - 樿: "椫", - 橈: "桡", - 橋: "桥", - 機: "机", - 橢: "椭", - 橫: "横", - 橯: "𣓿", - 檁: "檩", - 檉: "柽", - 檔: "档", - 檜: "桧", - 檟: "槚", - 檢: "检", - 檣: "樯", - 檭: "𣘴", - 檮: "梼", - 檯: "台", - 檳: "槟", - 檵: "𪲛", - 檸: "柠", - 檻: "槛", - 櫃: "柜", - 櫅: "𪲎", - 櫍: "𬃊", - 櫓: "橹", - 櫚: "榈", - 櫛: "栉", - 櫝: "椟", - 櫞: "橼", - 櫟: "栎", - 櫠: "𪲮", - 櫥: "橱", - 櫧: "槠", - 櫨: "栌", - 櫪: "枥", - 櫫: "橥", - 櫬: "榇", - 櫱: "蘖", - 櫳: "栊", - 櫸: "榉", - 櫻: "樱", - 欄: "栏", - 欅: "榉", - 欇: "𪳍", - 權: "权", - 欍: "𣐤", - 欏: "椤", - 欐: "𪲔", - 欑: "𪴙", - 欒: "栾", - 欓: "𣗋", - 欖: "榄", - 欘: "𣚚", - 欞: "棂", - 欽: "钦", - 歎: "叹", - 歐: "欧", - 歟: "欤", - 歡: "欢", - 歲: "岁", - 歷: "历", - 歸: "归", - 歿: "殁", - 殘: "残", - 殞: "殒", - 殢: "𣨼", - 殤: "殇", - 殨: "㱮", - 殫: "殚", - 殭: "僵", - 殮: "殓", - 殯: "殡", - 殰: "㱩", - 殲: "歼", - 殺: "杀", - 殻: "壳", - 殼: "壳", - 毀: "毁", - 毆: "殴", - 毊: "𪵑", - 毿: "毵", - 氂: "牦", - 氈: "毡", - 氌: "氇", - 氣: "气", - 氫: "氢", - 氬: "氩", - 氭: "𣱝", - 氳: "氲", - 氾: "泛", - 汎: "泛", - 汙: "污", - 決: "决", - 沒: "没", - 沖: "冲", - 況: "况", - 泝: "溯", - 洩: "泄", - 洶: "汹", - 浹: "浃", - 浿: "𬇙", - 涇: "泾", - 涗: "涚", - 涼: "凉", - 淒: "凄", - 淚: "泪", - 淥: "渌", - 淨: "净", - 淩: "凌", - 淪: "沦", - 淵: "渊", - 淶: "涞", - 淺: "浅", - 渙: "涣", - 減: "减", - 渢: "沨", - 渦: "涡", - 測: "测", - 渾: "浑", - 湊: "凑", - 湋: "𣲗", - 湞: "浈", - 湧: "涌", - 湯: "汤", - 溈: "沩", - 準: "准", - 溝: "沟", - 溡: "𪶄", - 溫: "温", - 溮: "浉", - 溳: "涢", - 溼: "湿", - 滄: "沧", - 滅: "灭", - 滌: "涤", - 滎: "荥", - 滙: "汇", - 滬: "沪", - 滯: "滞", - 滲: "渗", - 滷: "卤", - 滸: "浒", - 滻: "浐", - 滾: "滚", - 滿: "满", - 漁: "渔", - 漊: "溇", - 漍: "𬇹", - 漚: "沤", - 漢: "汉", - 漣: "涟", - 漬: "渍", - 漲: "涨", - 漵: "溆", - 漸: "渐", - 漿: "浆", - 潁: "颍", - 潑: "泼", - 潔: "洁", - 潕: "𣲘", - 潙: "沩", - 潚: "㴋", - 潛: "潜", - 潣: "𫞗", - 潤: "润", - 潯: "浔", - 潰: "溃", - 潷: "滗", - 潿: "涠", - 澀: "涩", - 澅: "𣶩", - 澆: "浇", - 澇: "涝", - 澐: "沄", - 澗: "涧", - 澠: "渑", - 澤: "泽", - 澦: "滪", - 澩: "泶", - 澫: "𬇕", - 澬: "𫞚", - 澮: "浍", - 澱: "淀", - 澾: "㳠", - 濁: "浊", - 濃: "浓", - 濄: "㳡", - 濆: "𣸣", - 濕: "湿", - 濘: "泞", - 濚: "溁", - 濛: "蒙", - 濜: "浕", - 濟: "济", - 濤: "涛", - 濧: "㳔", - 濫: "滥", - 濰: "潍", - 濱: "滨", - 濺: "溅", - 濼: "泺", - 濾: "滤", - 濿: "𪵱", - 瀂: "澛", - 瀃: "𣽷", - 瀅: "滢", - 瀆: "渎", - 瀇: "㲿", - 瀉: "泻", - 瀋: "沈", - 瀏: "浏", - 瀕: "濒", - 瀘: "泸", - 瀝: "沥", - 瀟: "潇", - 瀠: "潆", - 瀦: "潴", - 瀧: "泷", - 瀨: "濑", - 瀰: "弥", - 瀲: "潋", - 瀾: "澜", - 灃: "沣", - 灄: "滠", - 灍: "𫞝", - 灑: "洒", - 灒: "𪷽", - 灕: "漓", - 灘: "滩", - 灙: "𣺼", - 灝: "灏", - 灡: "㳕", - 灣: "湾", - 灤: "滦", - 灧: "滟", - 灩: "滟", - 災: "灾", - 為: "为", - 烏: "乌", - 烴: "烃", - 無: "无", - 煇: "𪸩", - 煉: "炼", - 煒: "炜", - 煙: "烟", - 煢: "茕", - 煥: "焕", - 煩: "烦", - 煬: "炀", - 煱: "㶽", - 熂: "𪸕", - 熅: "煴", - 熉: "𤈶", - 熌: "𤇄", - 熒: "荧", - 熓: "𤆡", - 熗: "炝", - 熚: "𤇹", - 熡: "𤋏", - 熰: "𬉼", - 熱: "热", - 熲: "颎", - 熾: "炽", - 燀: "𬊤", - 燁: "烨", - 燈: "灯", - 燉: "炖", - 燒: "烧", - 燖: "𬊈", - 燙: "烫", - 燜: "焖", - 營: "营", - 燦: "灿", - 燬: "毁", - 燭: "烛", - 燴: "烩", - 燶: "㶶", - 燻: "熏", - 燼: "烬", - 燾: "焘", - 爃: "𫞡", - 爄: "𤇃", - 爇: "𦶟", - 爍: "烁", - 爐: "炉", - 爖: "𤇭", - 爛: "烂", - 爥: "𪹳", - 爧: "𫞠", - 爭: "争", - 爲: "为", - 爺: "爷", - 爾: "尔", - 牀: "床", - 牆: "墙", - 牘: "牍", - 牴: "牴", - 牽: "牵", - 犖: "荦", - 犛: "牦", - 犞: "𪺭", - 犢: "犊", - 犧: "牺", - 狀: "状", - 狹: "狭", - 狽: "狈", - 猌: "𪺽", - 猙: "狰", - 猶: "犹", - 猻: "狲", - 獁: "犸", - 獃: "呆", - 獄: "狱", - 獅: "狮", - 獊: "𪺷", - 獎: "奖", - 獨: "独", - 獩: "𤞃", - 獪: "狯", - 獫: "猃", - 獮: "狝", - 獰: "狞", - 獱: "㺍", - 獲: "获", - 獵: "猎", - 獷: "犷", - 獸: "兽", - 獺: "獭", - 獻: "献", - 獼: "猕", - 玀: "猡", - 玁: "𤞤", - 珼: "𫞥", - 現: "现", - 琱: "雕", - 琺: "珐", - 琿: "珲", - 瑋: "玮", - 瑒: "玚", - 瑣: "琐", - 瑤: "瑶", - 瑩: "莹", - 瑪: "玛", - 瑲: "玱", - 瑻: "𪻲", - 瑽: "𪻐", - 璉: "琏", - 璊: "𫞩", - 璕: "𬍤", - 璗: "𬍡", - 璝: "𪻺", - 璡: "琎", - 璣: "玑", - 璦: "瑷", - 璫: "珰", - 璯: "㻅", - 環: "环", - 璵: "玙", - 璸: "瑸", - 璼: "𫞨", - 璽: "玺", - 璾: "𫞦", - 璿: "璇", - 瓄: "𪻨", - 瓅: "𬍛", - 瓊: "琼", - 瓏: "珑", - 瓔: "璎", - 瓕: "𤦀", - 瓚: "瓒", - 瓛: "𤩽", - 甌: "瓯", - 甕: "瓮", - 產: "产", - 産: "产", - 甦: "苏", - 甯: "宁", - 畝: "亩", - 畢: "毕", - 畫: "画", - 異: "异", - 畵: "画", - 當: "当", - 畼: "𪽈", - 疇: "畴", - 疊: "叠", - 痙: "痉", - 痠: "酸", - 痮: "𪽪", - 痾: "疴", - 瘂: "痖", - 瘋: "疯", - 瘍: "疡", - 瘓: "痪", - 瘞: "瘗", - 瘡: "疮", - 瘧: "疟", - 瘮: "瘆", - 瘱: "𪽷", - 瘲: "疭", - 瘺: "瘘", - 瘻: "瘘", - 療: "疗", - 癆: "痨", - 癇: "痫", - 癉: "瘅", - 癐: "𤶊", - 癒: "愈", - 癘: "疠", - 癟: "瘪", - 癡: "痴", - 癢: "痒", - 癤: "疖", - 癥: "症", - 癧: "疬", - 癩: "癞", - 癬: "癣", - 癭: "瘿", - 癮: "瘾", - 癰: "痈", - 癱: "瘫", - 癲: "癫", - 發: "发", - 皁: "皂", - 皚: "皑", - 皟: "𤾀", - 皰: "疱", - 皸: "皲", - 皺: "皱", - 盃: "杯", - 盜: "盗", - 盞: "盏", - 盡: "尽", - 監: "监", - 盤: "盘", - 盧: "卢", - 盨: "𪾔", - 盪: "荡", - 眝: "𪾣", - 眞: "真", - 眥: "眦", - 眾: "众", - 睍: "𪾢", - 睏: "困", - 睜: "睁", - 睞: "睐", - 瞘: "眍", - 瞜: "䁖", - 瞞: "瞒", - 瞤: "𥆧", - 瞭: "瞭", - 瞶: "瞆", - 瞼: "睑", - 矇: "蒙", - 矉: "𪾸", - 矑: "𪾦", - 矓: "眬", - 矚: "瞩", - 矯: "矫", - 硃: "朱", - 硜: "硁", - 硤: "硖", - 硨: "砗", - 硯: "砚", - 碕: "埼", - 碙: "𥐻", - 碩: "硕", - 碭: "砀", - 碸: "砜", - 確: "确", - 碼: "码", - 碽: "䂵", - 磑: "硙", - 磚: "砖", - 磠: "硵", - 磣: "碜", - 磧: "碛", - 磯: "矶", - 磽: "硗", - 磾: "䃅", - 礄: "硚", - 礆: "硷", - 礎: "础", - 礐: "𬒈", - 礒: "𥐟", - 礙: "碍", - 礦: "矿", - 礪: "砺", - 礫: "砾", - 礬: "矾", - 礮: "𪿫", - 礱: "砻", - 祇: "祇", - 祕: "秘", - 祿: "禄", - 禍: "祸", - 禎: "祯", - 禕: "祎", - 禡: "祃", - 禦: "御", - 禪: "禅", - 禮: "礼", - 禰: "祢", - 禱: "祷", - 禿: "秃", - 秈: "籼", - 稅: "税", - 稈: "秆", - 稏: "䅉", - 稜: "棱", - 稟: "禀", - 種: "种", - 稱: "称", - 穀: "谷", - 穇: "䅟", - 穌: "稣", - 積: "积", - 穎: "颖", - 穠: "秾", - 穡: "穑", - 穢: "秽", - 穩: "稳", - 穫: "获", - 穭: "穞", - 窩: "窝", - 窪: "洼", - 窮: "穷", - 窯: "窑", - 窵: "窎", - 窶: "窭", - 窺: "窥", - 竄: "窜", - 竅: "窍", - 竇: "窦", - 竈: "灶", - 竊: "窃", - 竚: "𥩟", - 竪: "竖", - 竱: "𫁟", - 競: "竞", - 筆: "笔", - 筍: "笋", - 筧: "笕", - 筴: "䇲", - 箇: "个", - 箋: "笺", - 箏: "筝", - 節: "节", - 範: "范", - 築: "筑", - 篋: "箧", - 篔: "筼", - 篘: "𥬠", - 篠: "筿", - 篢: "𬕂", - 篤: "笃", - 篩: "筛", - 篳: "筚", - 篸: "𥮾", - 簀: "箦", - 簂: "𫂆", - 簍: "篓", - 簑: "蓑", - 簞: "箪", - 簡: "简", - 簢: "𫂃", - 簣: "篑", - 簫: "箫", - 簹: "筜", - 簽: "签", - 簾: "帘", - 籃: "篮", - 籅: "𥫣", - 籋: "𥬞", - 籌: "筹", - 籔: "䉤", - 籙: "箓", - 籛: "篯", - 籜: "箨", - 籟: "籁", - 籠: "笼", - 籤: "签", - 籩: "笾", - 籪: "簖", - 籬: "篱", - 籮: "箩", - 籲: "吁", - 粵: "粤", - 糉: "粽", - 糝: "糁", - 糞: "粪", - 糧: "粮", - 糰: "团", - 糲: "粝", - 糴: "籴", - 糶: "粜", - 糹: "纟", - 糺: "𫄙", - 糾: "纠", - 紀: "纪", - 紂: "纣", - 紃: "𬘓", - 約: "约", - 紅: "红", - 紆: "纡", - 紇: "纥", - 紈: "纨", - 紉: "纫", - 紋: "纹", - 納: "纳", - 紐: "纽", - 紓: "纾", - 純: "纯", - 紕: "纰", - 紖: "纼", - 紗: "纱", - 紘: "纮", - 紙: "纸", - 級: "级", - 紛: "纷", - 紜: "纭", - 紝: "纴", - 紞: "𬘘", - 紟: "𫄛", - 紡: "纺", - 紬: "䌷", - 紮: "扎", - 細: "细", - 紱: "绂", - 紲: "绁", - 紳: "绅", - 紵: "纻", - 紹: "绍", - 紺: "绀", - 紼: "绋", - 紿: "绐", - 絀: "绌", - 絁: "𫄟", - 終: "终", - 絃: "弦", - 組: "组", - 絅: "䌹", - 絆: "绊", - 絍: "𫟃", - 絎: "绗", - 結: "结", - 絕: "绝", - 絙: "𫄠", - 絛: "绦", - 絝: "绔", - 絞: "绞", - 絡: "络", - 絢: "绚", - 絥: "𫄢", - 給: "给", - 絧: "𫄡", - 絨: "绒", - 絪: "𬘡", - 絰: "绖", - 統: "统", - 絲: "丝", - 絳: "绛", - 絶: "绝", - 絹: "绢", - 絺: "𫄨", - 綀: "𦈌", - 綁: "绑", - 綃: "绡", - 綄: "𬘫", - 綆: "绠", - 綇: "𦈋", - 綈: "绨", - 綉: "绣", - 綋: "𫟄", - 綌: "绤", - 綎: "𬘩", - 綏: "绥", - 綐: "䌼", - 綑: "捆", - 經: "经", - 綖: "𫄧", - 綜: "综", - 綝: "𬘭", - 綞: "缍", - 綟: "𫄫", - 綠: "绿", - 綡: "𫟅", - 綢: "绸", - 綣: "绻", - 綧: "𬘯", - 綪: "𬘬", - 綫: "线", - 綬: "绶", - 維: "维", - 綯: "绹", - 綰: "绾", - 綱: "纲", - 網: "网", - 綳: "绷", - 綴: "缀", - 綵: "彩", - 綸: "纶", - 綹: "绺", - 綺: "绮", - 綻: "绽", - 綽: "绰", - 綾: "绫", - 綿: "绵", - 緄: "绲", - 緇: "缁", - 緊: "紧", - 緋: "绯", - 緍: "𦈏", - 緑: "绿", - 緒: "绪", - 緓: "绬", - 緔: "绱", - 緗: "缃", - 緘: "缄", - 緙: "缂", - 線: "线", - 緝: "缉", - 緞: "缎", - 緟: "𫟆", - 締: "缔", - 緡: "缗", - 緣: "缘", - 緤: "𫄬", - 緦: "缌", - 編: "编", - 緩: "缓", - 緬: "缅", - 緮: "𫄭", - 緯: "纬", - 緰: "𦈕", - 緱: "缑", - 緲: "缈", - 練: "练", - 緶: "缏", - 緷: "𦈉", - 緸: "𦈑", - 緹: "缇", - 緻: "致", - 緼: "缊", - 縈: "萦", - 縉: "缙", - 縊: "缢", - 縋: "缒", - 縍: "𫄰", - 縎: "𦈔", - 縐: "绉", - 縑: "缣", - 縕: "缊", - 縗: "缞", - 縛: "缚", - 縝: "缜", - 縞: "缟", - 縟: "缛", - 縣: "县", - 縧: "绦", - 縫: "缝", - 縬: "𦈚", - 縭: "缡", - 縮: "缩", - 縯: "𬙂", - 縰: "𫄳", - 縱: "纵", - 縲: "缧", - 縳: "䌸", - 縴: "纤", - 縵: "缦", - 縶: "絷", - 縷: "缕", - 縸: "𫄲", - 縹: "缥", - 縺: "𦈐", - 總: "总", - 績: "绩", - 繂: "𫄴", - 繃: "绷", - 繅: "缫", - 繆: "缪", - 繈: "𫄶", - 繏: "𦈝", - 繐: "𰬸", - 繒: "缯", - 繓: "𦈛", - 織: "织", - 繕: "缮", - 繚: "缭", - 繞: "绕", - 繟: "𦈎", - 繡: "绣", - 繢: "缋", - 繨: "𫄤", - 繩: "绳", - 繪: "绘", - 繫: "系", - 繬: "𫄱", - 繭: "茧", - 繮: "缰", - 繯: "缳", - 繰: "缲", - 繳: "缴", - 繶: "𫄷", - 繷: "𫄣", - 繸: "䍁", - 繹: "绎", - 繻: "𦈡", - 繼: "继", - 繽: "缤", - 繾: "缱", - 繿: "䍀", - 纁: "𫄸", - 纆: "𬙊", - 纇: "颣", - 纈: "缬", - 纊: "纩", - 續: "续", - 纍: "累", - 纏: "缠", - 纓: "缨", - 纔: "才", - 纕: "𬙋", - 纖: "纤", - 纗: "𫄹", - 纘: "缵", - 纚: "𫄥", - 纜: "缆", - 缽: "钵", - 罃: "䓨", - 罈: "坛", - 罌: "罂", - 罎: "坛", - 罰: "罚", - 罵: "骂", - 罷: "罢", - 羅: "罗", - 羆: "罴", - 羈: "羁", - 羋: "芈", - 羣: "群", - 羥: "羟", - 羨: "羡", - 義: "义", - 羵: "𫅗", - 羶: "膻", - 習: "习", - 翫: "玩", - 翬: "翚", - 翹: "翘", - 翽: "翙", - 耬: "耧", - 耮: "耢", - 聖: "圣", - 聞: "闻", - 聯: "联", - 聰: "聪", - 聲: "声", - 聳: "耸", - 聵: "聩", - 聶: "聂", - 職: "职", - 聹: "聍", - 聻: "𫆏", - 聽: "听", - 聾: "聋", - 肅: "肃", - 脅: "胁", - 脈: "脉", - 脛: "胫", - 脣: "唇", - 脥: "𣍰", - 脩: "修", - 脫: "脱", - 脹: "胀", - 腎: "肾", - 腖: "胨", - 腡: "脶", - 腦: "脑", - 腪: "𣍯", - 腫: "肿", - 腳: "脚", - 腸: "肠", - 膃: "腽", - 膕: "腘", - 膚: "肤", - 膞: "䏝", - 膠: "胶", - 膢: "𦝼", - 膩: "腻", - 膹: "𪱥", - 膽: "胆", - 膾: "脍", - 膿: "脓", - 臉: "脸", - 臍: "脐", - 臏: "膑", - 臗: "𣎑", - 臘: "腊", - 臚: "胪", - 臟: "脏", - 臠: "脔", - 臢: "臜", - 臥: "卧", - 臨: "临", - 臺: "台", - 與: "与", - 興: "兴", - 舉: "举", - 舊: "旧", - 舘: "馆", - 艙: "舱", - 艣: "𫇛", - 艤: "舣", - 艦: "舰", - 艫: "舻", - 艱: "艰", - 艷: "艳", - 芻: "刍", - 苧: "苎", - 茲: "兹", - 荊: "荆", - 莊: "庄", - 莖: "茎", - 莢: "荚", - 莧: "苋", - 菕: "𰰨", - 華: "华", - 菴: "庵", - 菸: "烟", - 萇: "苌", - 萊: "莱", - 萬: "万", - 萴: "荝", - 萵: "莴", - 葉: "叶", - 葒: "荭", - 葝: "𫈎", - 葤: "荮", - 葦: "苇", - 葯: "药", - 葷: "荤", - 蒍: "𫇭", - 蒐: "搜", - 蒓: "莼", - 蒔: "莳", - 蒕: "蒀", - 蒞: "莅", - 蒭: "𫇴", - 蒼: "苍", - 蓀: "荪", - 蓆: "席", - 蓋: "盖", - 蓧: "𦰏", - 蓮: "莲", - 蓯: "苁", - 蓴: "莼", - 蓽: "荜", - 蔄: "𬜬", - 蔔: "卜", - 蔘: "参", - 蔞: "蒌", - 蔣: "蒋", - 蔥: "葱", - 蔦: "茑", - 蔭: "荫", - 蔯: "𫈟", - 蔿: "𫇭", - 蕁: "荨", - 蕆: "蒇", - 蕎: "荞", - 蕒: "荬", - 蕓: "芸", - 蕕: "莸", - 蕘: "荛", - 蕝: "𫈵", - 蕢: "蒉", - 蕩: "荡", - 蕪: "芜", - 蕭: "萧", - 蕳: "𫈉", - 蕷: "蓣", - 蕽: "𫇽", - 薀: "蕰", - 薆: "𫉁", - 薈: "荟", - 薊: "蓟", - 薌: "芗", - 薑: "姜", - 薔: "蔷", - 薘: "荙", - 薟: "莶", - 薦: "荐", - 薩: "萨", - 薳: "䓕", - 薴: "苧", - 薵: "䓓", - 薹: "苔", - 薺: "荠", - 藉: "藉", - 藍: "蓝", - 藎: "荩", - 藝: "艺", - 藥: "药", - 藪: "薮", - 藭: "䓖", - 藴: "蕴", - 藶: "苈", - 藷: "𫉄", - 藹: "蔼", - 藺: "蔺", - 蘀: "萚", - 蘄: "蕲", - 蘆: "芦", - 蘇: "苏", - 蘊: "蕴", - 蘋: "苹", - 蘚: "藓", - 蘞: "蔹", - 蘟: "𦻕", - 蘢: "茏", - 蘭: "兰", - 蘺: "蓠", - 蘿: "萝", - 虆: "蔂", - 虉: "𬟁", - 處: "处", - 虛: "虚", - 虜: "虏", - 號: "号", - 虧: "亏", - 虯: "虬", - 蛺: "蛱", - 蛻: "蜕", - 蜆: "蚬", - 蝀: "𬟽", - 蝕: "蚀", - 蝟: "猬", - 蝦: "虾", - 蝨: "虱", - 蝸: "蜗", - 螄: "蛳", - 螞: "蚂", - 螢: "萤", - 螮: "䗖", - 螻: "蝼", - 螿: "螀", - 蟂: "𫋇", - 蟄: "蛰", - 蟈: "蝈", - 蟎: "螨", - 蟘: "𫋌", - 蟜: "𫊸", - 蟣: "虮", - 蟬: "蝉", - 蟯: "蛲", - 蟲: "虫", - 蟳: "𫊻", - 蟶: "蛏", - 蟻: "蚁", - 蠀: "𧏗", - 蠁: "蚃", - 蠅: "蝇", - 蠆: "虿", - 蠍: "蝎", - 蠐: "蛴", - 蠑: "蝾", - 蠔: "蚝", - 蠙: "𧏖", - 蠟: "蜡", - 蠣: "蛎", - 蠦: "𫊮", - 蠨: "蟏", - 蠱: "蛊", - 蠶: "蚕", - 蠻: "蛮", - 蠾: "𧑏", - 衆: "众", - 衊: "蔑", - 術: "术", - 衕: "同", - 衚: "胡", - 衛: "卫", - 衝: "冲", - 衹: "衹", - 袞: "衮", - 裊: "袅", - 裏: "里", - 補: "补", - 裝: "装", - 裡: "里", - 製: "制", - 複: "复", - 褌: "裈", - 褘: "袆", - 褲: "裤", - 褳: "裢", - 褸: "褛", - 褻: "亵", - 襀: "𫌀", - 襇: "裥", - 襉: "裥", - 襏: "袯", - 襓: "𫋹", - 襖: "袄", - 襗: "𫋷", - 襘: "𫋻", - 襝: "裣", - 襠: "裆", - 襤: "褴", - 襪: "袜", - 襬: "摆", - 襯: "衬", - 襰: "𧝝", - 襲: "袭", - 襴: "襕", - 襵: "𫌇", - 覆: "覆", - 覈: "核", - 見: "见", - 覎: "觃", - 規: "规", - 覓: "觅", - 視: "视", - 覘: "觇", - 覛: "𫌪", - 覡: "觋", - 覥: "觍", - 覦: "觎", - 親: "亲", - 覬: "觊", - 覯: "觏", - 覲: "觐", - 覷: "觑", - 覹: "𫌭", - 覺: "觉", - 覼: "𫌨", - 覽: "览", - 覿: "觌", - 觀: "观", - 觴: "觞", - 觶: "觯", - 觸: "触", - 訁: "讠", - 訂: "订", - 訃: "讣", - 計: "计", - 訊: "讯", - 訌: "讧", - 討: "讨", - 訏: "𬣙", - 訐: "讦", - 訑: "𫍙", - 訒: "讱", - 訓: "训", - 訕: "讪", - 訖: "讫", - 託: "托", - 記: "记", - 訛: "讹", - 訜: "𫍛", - 訝: "讶", - 訞: "𫍚", - 訟: "讼", - 訢: "䜣", - 訣: "诀", - 訥: "讷", - 訨: "𫟞", - 訩: "讻", - 訪: "访", - 設: "设", - 許: "许", - 訴: "诉", - 訶: "诃", - 診: "诊", - 註: "注", - 証: "证", - 詀: "𧮪", - 詁: "诂", - 詆: "诋", - 詊: "𫟟", - 詎: "讵", - 詐: "诈", - 詑: "𫍡", - 詒: "诒", - 詓: "𫍜", - 詔: "诏", - 評: "评", - 詖: "诐", - 詗: "诇", - 詘: "诎", - 詛: "诅", - 詝: "𬣞", - 詞: "词", - 詠: "咏", - 詡: "诩", - 詢: "询", - 詣: "诣", - 試: "试", - 詩: "诗", - 詪: "𬣳", - 詫: "诧", - 詬: "诟", - 詭: "诡", - 詮: "诠", - 詰: "诘", - 話: "话", - 該: "该", - 詳: "详", - 詵: "诜", - 詷: "𫍣", - 詼: "诙", - 詿: "诖", - 誂: "𫍥", - 誄: "诔", - 誅: "诛", - 誆: "诓", - 誇: "夸", - 誋: "𫍪", - 誌: "志", - 認: "认", - 誑: "诳", - 誒: "诶", - 誕: "诞", - 誘: "诱", - 誚: "诮", - 語: "语", - 誠: "诚", - 誡: "诫", - 誣: "诬", - 誤: "误", - 誥: "诰", - 誦: "诵", - 誨: "诲", - 說: "说", - 誫: "𫍨", - 説: "说", - 誰: "谁", - 課: "课", - 誳: "𫍮", - 誴: "𫟡", - 誶: "谇", - 誷: "𫍬", - 誹: "诽", - 誺: "𫍧", - 誼: "谊", - 誾: "訚", - 調: "调", - 諂: "谄", - 諄: "谆", - 談: "谈", - 諉: "诿", - 請: "请", - 諍: "诤", - 諏: "诹", - 諑: "诼", - 諒: "谅", - 諓: "𬣡", - 論: "论", - 諗: "谂", - 諛: "谀", - 諜: "谍", - 諝: "谞", - 諞: "谝", - 諟: "𬤊", - 諡: "谥", - 諢: "诨", - 諣: "𫍩", - 諤: "谔", - 諥: "𫍳", - 諦: "谛", - 諧: "谐", - 諫: "谏", - 諭: "谕", - 諮: "咨", - 諯: "𫍱", - 諰: "𫍰", - 諱: "讳", - 諲: "𬤇", - 諳: "谙", - 諴: "𫍯", - 諶: "谌", - 諷: "讽", - 諸: "诸", - 諺: "谚", - 諼: "谖", - 諾: "诺", - 謀: "谋", - 謁: "谒", - 謂: "谓", - 謄: "誊", - 謅: "诌", - 謆: "𫍸", - 謉: "𫍷", - 謊: "谎", - 謎: "谜", - 謏: "𫍲", - 謐: "谧", - 謔: "谑", - 謖: "谡", - 謗: "谤", - 謙: "谦", - 謚: "谥", - 講: "讲", - 謝: "谢", - 謠: "谣", - 謡: "谣", - 謨: "谟", - 謫: "谪", - 謬: "谬", - 謭: "谫", - 謯: "𫍹", - 謱: "𫍴", - 謳: "讴", - 謸: "𫍵", - 謹: "谨", - 謾: "谩", - 譁: "哗", - 譂: "𫟠", - 譅: "𰶎", - 譆: "𫍻", - 證: "证", - 譊: "𫍢", - 譎: "谲", - 譏: "讥", - 譑: "𫍤", - 譓: "𬤝", - 譖: "谮", - 識: "识", - 譙: "谯", - 譚: "谭", - 譜: "谱", - 譞: "𫍽", - 譟: "噪", - 譨: "𫍦", - 譫: "谵", - 譭: "毁", - 譯: "译", - 議: "议", - 譴: "谴", - 護: "护", - 譸: "诪", - 譽: "誉", - 譾: "谫", - 讀: "读", - 讅: "谉", - 變: "变", - 讋: "詟", - 讌: "䜩", - 讎: "雠", - 讒: "谗", - 讓: "让", - 讕: "谰", - 讖: "谶", - 讚: "赞", - 讜: "谠", - 讞: "谳", - 豈: "岂", - 豎: "竖", - 豐: "丰", - 豔: "艳", - 豬: "猪", - 豵: "𫎆", - 豶: "豮", - 貓: "猫", - 貗: "𫎌", - 貙: "䝙", - 貝: "贝", - 貞: "贞", - 貟: "贠", - 負: "负", - 財: "财", - 貢: "贡", - 貧: "贫", - 貨: "货", - 販: "贩", - 貪: "贪", - 貫: "贯", - 責: "责", - 貯: "贮", - 貰: "贳", - 貲: "赀", - 貳: "贰", - 貴: "贵", - 貶: "贬", - 買: "买", - 貸: "贷", - 貺: "贶", - 費: "费", - 貼: "贴", - 貽: "贻", - 貿: "贸", - 賀: "贺", - 賁: "贲", - 賂: "赂", - 賃: "赁", - 賄: "贿", - 賅: "赅", - 資: "资", - 賈: "贾", - 賊: "贼", - 賑: "赈", - 賒: "赊", - 賓: "宾", - 賕: "赇", - 賙: "赒", - 賚: "赉", - 賜: "赐", - 賝: "𫎩", - 賞: "赏", - 賟: "𧹖", - 賠: "赔", - 賡: "赓", - 賢: "贤", - 賣: "卖", - 賤: "贱", - 賦: "赋", - 賧: "赕", - 質: "质", - 賫: "赍", - 賬: "账", - 賭: "赌", - 賰: "䞐", - 賴: "赖", - 賵: "赗", - 賺: "赚", - 賻: "赙", - 購: "购", - 賽: "赛", - 賾: "赜", - 贃: "𧹗", - 贄: "贽", - 贅: "赘", - 贇: "赟", - 贈: "赠", - 贉: "𫎫", - 贊: "赞", - 贋: "赝", - 贍: "赡", - 贏: "赢", - 贐: "赆", - 贑: "𫎬", - 贓: "赃", - 贔: "赑", - 贖: "赎", - 贗: "赝", - 贚: "𫎦", - 贛: "赣", - 贜: "赃", - 赬: "赪", - 趕: "赶", - 趙: "赵", - 趨: "趋", - 趲: "趱", - 跡: "迹", - 踐: "践", - 踰: "逾", - 踴: "踊", - 蹌: "跄", - 蹔: "𫏐", - 蹕: "跸", - 蹟: "迹", - 蹠: "跖", - 蹣: "蹒", - 蹤: "踪", - 蹳: "𫏆", - 蹺: "跷", - 蹻: "𫏋", - 躂: "跶", - 躉: "趸", - 躊: "踌", - 躋: "跻", - 躍: "跃", - 躎: "䟢", - 躑: "踯", - 躒: "跞", - 躓: "踬", - 躕: "蹰", - 躘: "𨀁", - 躚: "跹", - 躝: "𨅬", - 躡: "蹑", - 躥: "蹿", - 躦: "躜", - 躪: "躏", - 軀: "躯", - 軉: "𨉗", - 車: "车", - 軋: "轧", - 軌: "轨", - 軍: "军", - 軏: "𫐄", - 軑: "轪", - 軒: "轩", - 軔: "轫", - 軕: "𫐅", - 軗: "𨐅", - 軛: "轭", - 軜: "𫐇", - 軝: "𬨂", - 軟: "软", - 軤: "轷", - 軨: "𫐉", - 軫: "轸", - 軬: "𫐊", - 軲: "轱", - 軷: "𫐈", - 軸: "轴", - 軹: "轵", - 軺: "轺", - 軻: "轲", - 軼: "轶", - 軾: "轼", - 軿: "𫐌", - 較: "较", - 輄: "𨐈", - 輅: "辂", - 輇: "辁", - 輈: "辀", - 載: "载", - 輊: "轾", - 輋: "𪨶", - 輒: "辄", - 輓: "挽", - 輔: "辅", - 輕: "轻", - 輖: "𫐏", - 輗: "𫐐", - 輛: "辆", - 輜: "辎", - 輝: "辉", - 輞: "辋", - 輟: "辍", - 輢: "𫐎", - 輥: "辊", - 輦: "辇", - 輨: "𫐑", - 輩: "辈", - 輪: "轮", - 輬: "辌", - 輮: "𫐓", - 輯: "辑", - 輳: "辏", - 輶: "𬨎", - 輷: "𫐒", - 輸: "输", - 輻: "辐", - 輼: "辒", - 輾: "辗", - 輿: "舆", - 轀: "辒", - 轂: "毂", - 轄: "辖", - 轅: "辕", - 轆: "辘", - 轇: "𫐖", - 轉: "转", - 轊: "𫐕", - 轍: "辙", - 轎: "轿", - 轐: "𫐗", - 轔: "辚", - 轗: "𫐘", - 轟: "轰", - 轠: "𫐙", - 轡: "辔", - 轢: "轹", - 轣: "𫐆", - 轤: "轳", - 辦: "办", - 辭: "辞", - 辮: "辫", - 辯: "辩", - 農: "农", - 迴: "回", - 逕: "迳", - 這: "这", - 連: "连", - 週: "周", - 進: "进", - 遊: "游", - 運: "运", - 過: "过", - 達: "达", - 違: "违", - 遙: "遥", - 遜: "逊", - 遞: "递", - 遠: "远", - 遡: "溯", - 適: "适", - 遱: "𫐷", - 遲: "迟", - 遷: "迁", - 選: "选", - 遺: "遗", - 遼: "辽", - 邁: "迈", - 還: "还", - 邇: "迩", - 邊: "边", - 邏: "逻", - 邐: "逦", - 郟: "郏", - 郵: "邮", - 鄆: "郓", - 鄉: "乡", - 鄒: "邹", - 鄔: "邬", - 鄖: "郧", - 鄟: "𫑘", - 鄧: "邓", - 鄩: "𬩽", - 鄭: "郑", - 鄰: "邻", - 鄲: "郸", - 鄳: "𫑡", - 鄴: "邺", - 鄶: "郐", - 鄺: "邝", - 酇: "酂", - 酈: "郦", - 醃: "腌", - 醖: "酝", - 醜: "丑", - 醞: "酝", - 醟: "蒏", - 醣: "糖", - 醫: "医", - 醬: "酱", - 醱: "酦", - 醲: "𬪩", - 醶: "𫑷", - 釀: "酿", - 釁: "衅", - 釃: "酾", - 釅: "酽", - 釋: "释", - 釐: "厘", - 釒: "钅", - 釓: "钆", - 釔: "钇", - 釕: "钌", - 釗: "钊", - 釘: "钉", - 釙: "钋", - 釚: "𫟲", - 針: "针", - 釟: "𫓥", - 釣: "钓", - 釤: "钐", - 釦: "扣", - 釧: "钏", - 釨: "𫓦", - 釩: "钒", - 釲: "𫟳", - 釳: "𨰿", - 釴: "𬬩", - 釵: "钗", - 釷: "钍", - 釹: "钕", - 釺: "钎", - 釾: "䥺", - 釿: "𬬱", - 鈀: "钯", - 鈁: "钫", - 鈃: "钘", - 鈄: "钭", - 鈅: "钥", - 鈆: "𫓪", - 鈇: "𫓧", - 鈈: "钚", - 鈉: "钠", - 鈋: "𨱂", - 鈍: "钝", - 鈎: "钩", - 鈐: "钤", - 鈑: "钣", - 鈒: "钑", - 鈔: "钞", - 鈕: "钮", - 鈖: "𫟴", - 鈗: "𫟵", - 鈛: "𫓨", - 鈞: "钧", - 鈠: "𨱁", - 鈡: "钟", - 鈣: "钙", - 鈥: "钬", - 鈦: "钛", - 鈧: "钪", - 鈮: "铌", - 鈯: "𨱄", - 鈰: "铈", - 鈲: "𨱃", - 鈳: "钶", - 鈴: "铃", - 鈷: "钴", - 鈸: "钹", - 鈹: "铍", - 鈺: "钰", - 鈽: "钸", - 鈾: "铀", - 鈿: "钿", - 鉀: "钾", - 鉁: "𨱅", - 鉅: "巨", - 鉆: "钻", - 鉈: "铊", - 鉉: "铉", - 鉊: "𬬿", - 鉋: "铇", - 鉍: "铋", - 鉑: "铂", - 鉔: "𫓬", - 鉕: "钷", - 鉗: "钳", - 鉚: "铆", - 鉛: "铅", - 鉝: "𫟷", - 鉞: "钺", - 鉠: "𫓭", - 鉢: "钵", - 鉤: "钩", - 鉥: "𬬸", - 鉦: "钲", - 鉧: "𬭁", - 鉬: "钼", - 鉭: "钽", - 鉮: "𬬹", - 鉳: "锫", - 鉶: "铏", - 鉷: "𫟹", - 鉸: "铰", - 鉺: "铒", - 鉻: "铬", - 鉽: "𫟸", - 鉾: "𫓴", - 鉿: "铪", - 銀: "银", - 銁: "𫓲", - 銂: "𫟻", - 銃: "铳", - 銅: "铜", - 銈: "𫓯", - 銊: "𫓰", - 銍: "铚", - 銏: "𫟶", - 銑: "铣", - 銓: "铨", - 銖: "铢", - 銘: "铭", - 銚: "铫", - 銛: "铦", - 銜: "衔", - 銠: "铑", - 銣: "铷", - 銥: "铱", - 銦: "铟", - 銨: "铵", - 銩: "铥", - 銪: "铕", - 銫: "铯", - 銬: "铐", - 銱: "铞", - 銳: "锐", - 銶: "𨱇", - 銷: "销", - 銹: "锈", - 銻: "锑", - 銼: "锉", - 鋁: "铝", - 鋂: "𰾄", - 鋃: "锒", - 鋅: "锌", - 鋇: "钡", - 鋉: "𨱈", - 鋌: "铤", - 鋏: "铗", - 鋐: "𬭎", - 鋒: "锋", - 鋗: "𫓶", - 鋙: "铻", - 鋝: "锊", - 鋟: "锓", - 鋠: "𫓵", - 鋣: "铘", - 鋤: "锄", - 鋥: "锃", - 鋦: "锔", - 鋨: "锇", - 鋩: "铓", - 鋪: "铺", - 鋭: "锐", - 鋮: "铖", - 鋯: "锆", - 鋰: "锂", - 鋱: "铽", - 鋶: "锍", - 鋸: "锯", - 鋹: "𬬮", - 鋼: "钢", - 錀: "𬬭", - 錁: "锞", - 錂: "𨱋", - 錄: "录", - 錆: "锖", - 錇: "锫", - 錈: "锩", - 錏: "铔", - 錐: "锥", - 錒: "锕", - 錕: "锟", - 錘: "锤", - 錙: "锱", - 錚: "铮", - 錛: "锛", - 錜: "𫓻", - 錝: "𫓽", - 錞: "𬭚", - 錟: "锬", - 錠: "锭", - 錡: "锜", - 錢: "钱", - 錤: "𫓹", - 錥: "𫓾", - 錦: "锦", - 錨: "锚", - 錩: "锠", - 錫: "锡", - 錮: "锢", - 錯: "错", - 録: "录", - 錳: "锰", - 錶: "表", - 錸: "铼", - 錼: "镎", - 錽: "𫓸", - 鍀: "锝", - 鍁: "锨", - 鍃: "锪", - 鍄: "𨱉", - 鍅: "钫", - 鍆: "钔", - 鍇: "锴", - 鍈: "锳", - 鍉: "𫔂", - 鍊: "炼", - 鍋: "锅", - 鍍: "镀", - 鍒: "𫔄", - 鍔: "锷", - 鍘: "铡", - 鍚: "钖", - 鍛: "锻", - 鍠: "锽", - 鍤: "锸", - 鍥: "锲", - 鍩: "锘", - 鍬: "锹", - 鍭: "𬭤", - 鍮: "𨱎", - 鍰: "锾", - 鍵: "键", - 鍶: "锶", - 鍺: "锗", - 鍼: "针", - 鍾: "钟", - 鎂: "镁", - 鎄: "锿", - 鎇: "镅", - 鎈: "𫟿", - 鎊: "镑", - 鎌: "镰", - 鎍: "𫔅", - 鎓: "𬭩", - 鎔: "镕", - 鎖: "锁", - 鎘: "镉", - 鎙: "𫔈", - 鎚: "锤", - 鎛: "镈", - 鎝: "𨱏", - 鎞: "𫔇", - 鎡: "镃", - 鎢: "钨", - 鎣: "蓥", - 鎦: "镏", - 鎧: "铠", - 鎩: "铩", - 鎪: "锼", - 鎬: "镐", - 鎭: "镇", - 鎮: "镇", - 鎯: "𨱍", - 鎰: "镒", - 鎲: "镋", - 鎳: "镍", - 鎵: "镓", - 鎶: "鿔", - 鎷: "𨰾", - 鎸: "镌", - 鎿: "镎", - 鏃: "镞", - 鏆: "𨱌", - 鏇: "旋", - 鏈: "链", - 鏉: "𨱒", - 鏌: "镆", - 鏍: "镙", - 鏏: "𬭬", - 鏐: "镠", - 鏑: "镝", - 鏗: "铿", - 鏘: "锵", - 鏚: "𬭭", - 鏜: "镗", - 鏝: "镘", - 鏞: "镛", - 鏟: "铲", - 鏡: "镜", - 鏢: "镖", - 鏤: "镂", - 鏥: "𫔊", - 鏦: "𫓩", - 鏨: "錾", - 鏰: "镚", - 鏵: "铧", - 鏷: "镤", - 鏹: "镪", - 鏺: "䥽", - 鏻: "𬭸", - 鏽: "锈", - 鏾: "𫔌", - 鐃: "铙", - 鐄: "𨱑", - 鐇: "𫔍", - 鐈: "𫓱", - 鐋: "铴", - 鐍: "𫔎", - 鐎: "𨱓", - 鐏: "𨱔", - 鐐: "镣", - 鐒: "铹", - 鐓: "镦", - 鐔: "镡", - 鐘: "钟", - 鐙: "镫", - 鐝: "镢", - 鐠: "镨", - 鐥: "䦅", - 鐦: "锎", - 鐧: "锏", - 鐨: "镄", - 鐩: "𬭼", - 鐪: "𫓺", - 鐫: "镌", - 鐮: "镰", - 鐯: "䦃", - 鐲: "镯", - 鐳: "镭", - 鐵: "铁", - 鐶: "镮", - 鐸: "铎", - 鐺: "铛", - 鐼: "𫔁", - 鐽: "𫟼", - 鐿: "镱", - 鑀: "𰾭", - 鑄: "铸", - 鑉: "𫠁", - 鑊: "镬", - 鑌: "镔", - 鑑: "鉴", - 鑒: "鉴", - 鑔: "镲", - 鑕: "锧", - 鑞: "镴", - 鑠: "铄", - 鑣: "镳", - 鑥: "镥", - 鑪: "𬬻", - 鑭: "镧", - 鑰: "钥", - 鑱: "镵", - 鑲: "镶", - 鑴: "𫔔", - 鑷: "镊", - 鑹: "镩", - 鑼: "锣", - 鑽: "钻", - 鑾: "銮", - 鑿: "凿", - 钁: "镢", - 钂: "镋", - 長: "长", - 門: "门", - 閂: "闩", - 閃: "闪", - 閆: "闫", - 閈: "闬", - 閉: "闭", - 開: "开", - 閌: "闶", - 閍: "𨸂", - 閎: "闳", - 閏: "闰", - 閐: "𨸃", - 閑: "闲", - 閒: "闲", - 間: "间", - 閔: "闵", - 閗: "𫔯", - 閘: "闸", - 閝: "𫠂", - 閞: "𫔰", - 閡: "阂", - 閣: "阁", - 閤: "合", - 閥: "阀", - 閨: "闺", - 閩: "闽", - 閫: "阃", - 閬: "阆", - 閭: "闾", - 閱: "阅", - 閲: "阅", - 閵: "𫔴", - 閶: "阊", - 閹: "阉", - 閻: "阎", - 閼: "阏", - 閽: "阍", - 閾: "阈", - 閿: "阌", - 闃: "阒", - 闆: "板", - 闇: "暗", - 闈: "闱", - 闉: "𬮱", - 闊: "阔", - 闋: "阕", - 闌: "阑", - 闍: "阇", - 闐: "阗", - 闑: "𫔶", - 闒: "阘", - 闓: "闿", - 闔: "阖", - 闕: "阙", - 闖: "闯", - 關: "关", - 闞: "阚", - 闠: "阓", - 闡: "阐", - 闢: "辟", - 闤: "阛", - 闥: "闼", - 阪: "阪", - 陘: "陉", - 陝: "陕", - 陞: "升", - 陣: "阵", - 陰: "阴", - 陳: "陈", - 陸: "陆", - 陽: "阳", - 隉: "陧", - 隊: "队", - 階: "阶", - 隑: "𬮿", - 隕: "陨", - 際: "际", - 隤: "𬯎", - 隨: "随", - 險: "险", - 隮: "𬯀", - 隯: "陦", - 隱: "隐", - 隴: "陇", - 隸: "隶", - 隻: "只", - 雋: "隽", - 雖: "虽", - 雙: "双", - 雛: "雏", - 雜: "杂", - 雞: "鸡", - 離: "离", - 難: "难", - 雲: "云", - 電: "电", - 霑: "沾", - 霢: "霡", - 霣: "𫕥", - 霧: "雾", - 霼: "𪵣", - 霽: "霁", - 靂: "雳", - 靄: "霭", - 靆: "叇", - 靈: "灵", - 靉: "叆", - 靚: "靓", - 靜: "静", - 靝: "靔", - 靦: "腼", - 靧: "𫖃", - 靨: "靥", - 鞏: "巩", - 鞝: "绱", - 鞦: "秋", - 鞽: "鞒", - 鞾: "𫖇", - 韁: "缰", - 韃: "鞑", - 韆: "千", - 韉: "鞯", - 韋: "韦", - 韌: "韧", - 韍: "韨", - 韓: "韩", - 韙: "韪", - 韚: "𫠅", - 韛: "𫖔", - 韜: "韬", - 韝: "鞲", - 韞: "韫", - 韠: "𫖒", - 韻: "韵", - 響: "响", - 頁: "页", - 頂: "顶", - 頃: "顷", - 項: "项", - 順: "顺", - 頇: "顸", - 須: "须", - 頊: "顼", - 頌: "颂", - 頍: "𫠆", - 頎: "颀", - 頏: "颃", - 預: "预", - 頑: "顽", - 頒: "颁", - 頓: "顿", - 頔: "𬱖", - 頗: "颇", - 領: "领", - 頜: "颌", - 頠: "𬱟", - 頡: "颉", - 頤: "颐", - 頦: "颏", - 頫: "𫖯", - 頭: "头", - 頮: "颒", - 頰: "颊", - 頲: "颋", - 頴: "颕", - 頵: "𫖳", - 頷: "颔", - 頸: "颈", - 頹: "颓", - 頻: "频", - 頽: "颓", - 顂: "𩓋", - 顃: "𩖖", - 顅: "𫖶", - 顆: "颗", - 題: "题", - 額: "额", - 顎: "颚", - 顏: "颜", - 顒: "颙", - 顓: "颛", - 顔: "颜", - 顗: "𫖮", - 願: "愿", - 顙: "颡", - 顛: "颠", - 類: "类", - 顢: "颟", - 顣: "𫖹", - 顥: "颢", - 顧: "顾", - 顫: "颤", - 顬: "颥", - 顯: "显", - 顰: "颦", - 顱: "颅", - 顳: "颞", - 顴: "颧", - 風: "风", - 颭: "飐", - 颮: "飑", - 颯: "飒", - 颰: "𩙥", - 颱: "台", - 颳: "刮", - 颶: "飓", - 颷: "𩙪", - 颸: "飔", - 颺: "飏", - 颻: "飖", - 颼: "飕", - 颾: "𩙫", - 飀: "飗", - 飄: "飘", - 飆: "飙", - 飈: "飚", - 飋: "𫗋", - 飛: "飞", - 飠: "饣", - 飢: "饥", - 飣: "饤", - 飥: "饦", - 飦: "𫗞", - 飩: "饨", - 飪: "饪", - 飫: "饫", - 飭: "饬", - 飯: "饭", - 飱: "飧", - 飲: "饮", - 飴: "饴", - 飵: "𫗢", - 飶: "𫗣", - 飼: "饲", - 飽: "饱", - 飾: "饰", - 飿: "饳", - 餃: "饺", - 餄: "饸", - 餅: "饼", - 餈: "糍", - 餉: "饷", - 養: "养", - 餌: "饵", - 餎: "饹", - 餏: "饻", - 餑: "饽", - 餒: "馁", - 餓: "饿", - 餔: "𫗦", - 餕: "馂", - 餖: "饾", - 餗: "𫗧", - 餘: "余", - 餚: "肴", - 餛: "馄", - 餜: "馃", - 餞: "饯", - 餡: "馅", - 餦: "𫗠", - 餧: "𫗪", - 館: "馆", - 餪: "𫗬", - 餫: "𫗥", - 餬: "糊", - 餭: "𫗮", - 餱: "糇", - 餳: "饧", - 餵: "喂", - 餶: "馉", - 餷: "馇", - 餸: "𩠌", - 餺: "馎", - 餼: "饩", - 餾: "馏", - 餿: "馊", - 饁: "馌", - 饃: "馍", - 饅: "馒", - 饈: "馐", - 饉: "馑", - 饊: "馓", - 饋: "馈", - 饌: "馔", - 饑: "饥", - 饒: "饶", - 饗: "飨", - 饘: "𫗴", - 饜: "餍", - 饞: "馋", - 饟: "𫗵", - 饠: "𫗩", - 饢: "馕", - 馬: "马", - 馭: "驭", - 馮: "冯", - 馯: "𫘛", - 馱: "驮", - 馳: "驰", - 馴: "驯", - 馹: "驲", - 馼: "𫘜", - 駁: "驳", - 駃: "𫘝", - 駉: "𬳶", - 駊: "𫘟", - 駎: "𩧨", - 駐: "驻", - 駑: "驽", - 駒: "驹", - 駓: "𬳵", - 駔: "驵", - 駕: "驾", - 駘: "骀", - 駙: "驸", - 駚: "𩧫", - 駛: "驶", - 駝: "驼", - 駞: "𫘞", - 駟: "驷", - 駡: "骂", - 駢: "骈", - 駤: "𫘠", - 駧: "𩧲", - 駩: "𩧴", - 駪: "𬳽", - 駫: "𫘡", - 駭: "骇", - 駰: "骃", - 駱: "骆", - 駶: "𩧺", - 駸: "骎", - 駻: "𫘣", - 駼: "𬳿", - 駿: "骏", - 騁: "骋", - 騂: "骍", - 騃: "𫘤", - 騄: "𫘧", - 騅: "骓", - 騉: "𫘥", - 騊: "𫘦", - 騌: "骔", - 騍: "骒", - 騎: "骑", - 騏: "骐", - 騑: "𬴂", - 騔: "𩨀", - 騖: "骛", - 騙: "骗", - 騚: "𩨊", - 騜: "𫘩", - 騝: "𩨃", - 騞: "𬴃", - 騟: "𩨈", - 騠: "𫘨", - 騤: "骙", - 騧: "䯄", - 騪: "𩨄", - 騫: "骞", - 騭: "骘", - 騮: "骝", - 騰: "腾", - 騱: "𫘬", - 騴: "𫘫", - 騵: "𫘪", - 騶: "驺", - 騷: "骚", - 騸: "骟", - 騻: "𫘭", - 騼: "𫠋", - 騾: "骡", - 驀: "蓦", - 驁: "骜", - 驂: "骖", - 驃: "骠", - 驄: "骢", - 驅: "驱", - 驊: "骅", - 驋: "𩧯", - 驌: "骕", - 驍: "骁", - 驎: "𬴊", - 驏: "骣", - 驓: "𫘯", - 驕: "骄", - 驗: "验", - 驙: "𫘰", - 驚: "惊", - 驛: "驿", - 驟: "骤", - 驢: "驴", - 驤: "骧", - 驥: "骥", - 驦: "骦", - 驨: "𫘱", - 驪: "骊", - 驫: "骉", - 骯: "肮", - 髏: "髅", - 髒: "脏", - 體: "体", - 髕: "髌", - 髖: "髋", - 髮: "发", - 鬆: "松", - 鬍: "胡", - 鬖: "𩭹", - 鬚: "须", - 鬠: "𫘽", - 鬢: "鬓", - 鬥: "斗", - 鬧: "闹", - 鬨: "哄", - 鬩: "阋", - 鬮: "阄", - 鬱: "郁", - 鬹: "鬶", - 魎: "魉", - 魘: "魇", - 魚: "鱼", - 魛: "鱽", - 魟: "𫚉", - 魢: "鱾", - 魥: "𩽹", - 魦: "𫚌", - 魨: "鲀", - 魯: "鲁", - 魴: "鲂", - 魵: "𫚍", - 魷: "鱿", - 魺: "鲄", - 魽: "𫠐", - 鮀: "𬶍", - 鮁: "鲅", - 鮃: "鲆", - 鮄: "𫚒", - 鮅: "𫚑", - 鮆: "𫚖", - 鮈: "𬶋", - 鮊: "鲌", - 鮋: "鲉", - 鮍: "鲏", - 鮎: "鲇", - 鮐: "鲐", - 鮑: "鲍", - 鮒: "鲋", - 鮓: "鲊", - 鮚: "鲒", - 鮜: "鲘", - 鮝: "鲞", - 鮞: "鲕", - 鮟: "𩽾", - 鮠: "𬶏", - 鮡: "𬶐", - 鮣: "䲟", - 鮤: "𫚓", - 鮦: "鲖", - 鮪: "鲔", - 鮫: "鲛", - 鮭: "鲑", - 鮮: "鲜", - 鮯: "𫚗", - 鮰: "𫚔", - 鮳: "鲓", - 鮵: "𫚛", - 鮶: "鲪", - 鮸: "3", - 鮺: "鲝", - 鮿: "𫚚", - 鯀: "鲧", - 鯁: "鲠", - 鯄: "𩾁", - 鯆: "𫚙", - 鯇: "鲩", - 鯉: "鲤", - 鯊: "鲨", - 鯒: "鲬", - 鯔: "鲻", - 鯕: "鲯", - 鯖: "鲭", - 鯗: "鲞", - 鯛: "鲷", - 鯝: "鲴", - 鯞: "𫚡", - 鯡: "鲱", - 鯢: "鲵", - 鯤: "鲲", - 鯧: "鲳", - 鯨: "鲸", - 鯪: "鲮", - 鯫: "鲰", - 鯬: "𫚞", - 鯰: "鲶", - 鯱: "𩾇", - 鯴: "鲺", - 鯶: "𩽼", - 鯷: "鳀", - 鯻: "𬶟", - 鯽: "鲫", - 鯾: "𫚣", - 鯿: "鳊", - 鰁: "鳈", - 鰂: "鲗", - 鰃: "鳂", - 鰆: "䲠", - 鰈: "鲽", - 鰉: "鳇", - 鰊: "𬶠", - 鰋: "𫚢", - 鰌: "䲡", - 鰍: "鳅", - 鰏: "鲾", - 鰐: "鳄", - 鰑: "𫚊", - 鰒: "鳆", - 鰓: "鳃", - 鰕: "𫚥", - 鰛: "鳁", - 鰜: "鳒", - 鰟: "鳑", - 鰠: "鳋", - 鰣: "鲥", - 鰤: "𫚕", - 鰥: "鳏", - 鰦: "𫚤", - 鰧: "䲢", - 鰨: "鳎", - 鰩: "鳐", - 鰫: "𫚦", - 鰭: "鳍", - 鰮: "鳁", - 鰱: "鲢", - 鰲: "鳌", - 鰳: "鳓", - 鰵: "鳘", - 鰶: "𬶭", - 鰷: "鲦", - 鰹: "鲣", - 鰺: "鲹", - 鰻: "鳗", - 鰼: "鳛", - 鰽: "𫚧", - 鰾: "鳔", - 鱀: "𬶨", - 鱂: "鳉", - 鱄: "𫚋", - 鱅: "鳙", - 鱆: "𫠒", - 鱇: "𩾌", - 鱈: "鳕", - 鱉: "鳖", - 鱊: "𫚪", - 鱒: "鳟", - 鱔: "鳝", - 鱖: "鳜", - 鱗: "鳞", - 鱘: "鲟", - 鱚: "𬶮", - 鱝: "鲼", - 鱟: "鲎", - 鱠: "鲙", - 鱢: "𫚫", - 鱣: "鳣", - 鱤: "鳡", - 鱧: "鳢", - 鱨: "鲿", - 鱭: "鲚", - 鱮: "𫚈", - 鱯: "鳠", - 鱲: "𫚭", - 鱷: "鳄", - 鱸: "鲈", - 鱺: "鲡", - 鳥: "鸟", - 鳧: "凫", - 鳩: "鸠", - 鳬: "凫", - 鳲: "鸤", - 鳳: "凤", - 鳴: "鸣", - 鳶: "鸢", - 鳷: "𫛛", - 鳼: "𪉃", - 鳽: "𫛚", - 鳾: "䴓", - 鴀: "𫛜", - 鴃: "𫛞", - 鴅: "𫛝", - 鴆: "鸩", - 鴇: "鸨", - 鴉: "鸦", - 鴐: "𫛤", - 鴒: "鸰", - 鴔: "𫛡", - 鴕: "鸵", - 鴗: "𫁡", - 鴛: "鸳", - 鴜: "𪉈", - 鴝: "鸲", - 鴞: "鸮", - 鴟: "鸱", - 鴣: "鸪", - 鴥: "𫛣", - 鴦: "鸯", - 鴨: "鸭", - 鴮: "𫛦", - 鴯: "鸸", - 鴰: "鸹", - 鴲: "𪉆", - 鴳: "𫛩", - 鴴: "鸻", - 鴷: "䴕", - 鴻: "鸿", - 鴽: "𫛪", - 鴿: "鸽", - 鵁: "䴔", - 鵂: "鸺", - 鵃: "鸼", - 鵊: "𫛥", - 鵏: "𬷕", - 鵐: "鹀", - 鵑: "鹃", - 鵒: "鹆", - 鵓: "鹁", - 鵚: "𪉍", - 鵜: "鹈", - 鵝: "鹅", - 鵟: "𫛭", - 鵠: "鹄", - 鵡: "鹉", - 鵧: "𫛨", - 鵩: "𫛳", - 鵪: "鹌", - 鵫: "𫛱", - 鵬: "鹏", - 鵮: "鹐", - 鵯: "鹎", - 鵰: "雕", - 鵲: "鹊", - 鵷: "鹓", - 鵾: "鹍", - 鶄: "䴖", - 鶇: "鸫", - 鶉: "鹑", - 鶊: "鹒", - 鶌: "𫛵", - 鶒: "𫛶", - 鶓: "鹋", - 鶖: "鹙", - 鶗: "𫛸", - 鶘: "鹕", - 鶚: "鹗", - 鶠: "𬸘", - 鶡: "鹖", - 鶥: "鹛", - 鶦: "𫛷", - 鶩: "鹜", - 鶪: "䴗", - 鶬: "鸧", - 鶭: "𫛯", - 鶯: "莺", - 鶰: "𫛫", - 鶱: "𬸣", - 鶲: "鹟", - 鶴: "鹤", - 鶹: "鹠", - 鶺: "鹡", - 鶻: "鹘", - 鶼: "鹣", - 鶿: "鹚", - 鷀: "鹚", - 鷁: "鹢", - 鷂: "鹞", - 鷄: "鸡", - 鷅: "𫛽", - 鷉: "䴘", - 鷊: "鹝", - 鷐: "𫜀", - 鷓: "鹧", - 鷔: "𪉑", - 鷖: "鹥", - 鷗: "鸥", - 鷙: "鸷", - 鷚: "鹨", - 鷟: "𬸦", - 鷣: "𫜃", - 鷤: "𫛴", - 鷥: "鸶", - 鷦: "鹪", - 鷨: "𪉊", - 鷩: "𫜁", - 鷫: "鹔", - 鷭: "𬸪", - 鷯: "鹩", - 鷲: "鹫", - 鷳: "鹇", - 鷴: "鹇", - 鷷: "𫜄", - 鷸: "鹬", - 鷹: "鹰", - 鷺: "鹭", - 鷽: "鸴", - 鷿: "𬸯", - 鸂: "㶉", - 鸇: "鹯", - 鸊: "䴙", - 鸋: "𫛢", - 鸌: "鹱", - 鸏: "鹲", - 鸑: "𬸚", - 鸕: "鸬", - 鸗: "𫛟", - 鸘: "鹴", - 鸚: "鹦", - 鸛: "鹳", - 鸝: "鹂", - 鸞: "鸾", - 鹵: "卤", - 鹹: "咸", - 鹺: "鹾", - 鹼: "碱", - 鹽: "盐", - 麗: "丽", - 麥: "麦", - 麨: "𪎊", - 麩: "麸", - 麪: "面", - 麫: "面", - 麬: "𤿲", - 麯: "曲", - 麲: "𪎉", - 麳: "𪎌", - 麴: "曲", - 麵: "面", - 麷: "𫜑", - 麼: "么", - 麽: "么", - 黃: "黄", - 黌: "黉", - 點: "点", - 黨: "党", - 黲: "黪", - 黴: "霉", - 黶: "黡", - 黷: "黩", - 黽: "黾", - 黿: "鼋", - 鼂: "鼌", - 鼉: "鼍", - 鼕: "冬", - 鼴: "鼹", - 齊: "齐", - 齋: "斋", - 齎: "赍", - 齏: "齑", - 齒: "齿", - 齔: "龀", - 齕: "龁", - 齗: "龂", - 齘: "𬹼", - 齙: "龅", - 齜: "龇", - 齟: "龃", - 齠: "龆", - 齡: "龄", - 齣: "出", - 齦: "龈", - 齧: "啮", - 齩: "𫜪", - 齪: "龊", - 齬: "龉", - 齭: "𫜭", - 齮: "𬺈", - 齯: "𫠜", - 齰: "𫜬", - 齲: "龋", - 齴: "𫜮", - 齶: "腭", - 齷: "龌", - 齼: "𬺓", - 齾: "𫜰", - 龍: "龙", - 龎: "厐", - 龐: "庞", - 龑: "䶮", - 龓: "𫜲", - 龔: "龚", - 龕: "龛", - 龜: "龟", - 龭: "𩨎", - 龯: "𨱆", - 鿁: "䜤", - 鿓: "鿒", - "𠁞": "𠀾", - "𠌥": "𠆿", - "𠏢": "𠉗", - "𠐊": "𫝋", - "𠗣": "㓆", - "𠞆": "𠛆", - "𠠎": "𠚳", - "𠬙": "𪠡", - "𠽃": "𪠺", - "𠿕": "𪜎", - "𡂡": "𪢒", - "𡃄": "𪡺", - "𡃕": "𠴛", - "𡃤": "𪢐", - "𡄔": "𠴢", - "𡄣": "𠵸", - "𡅏": "𠲥", - "𡅯": "𪢖", - "𡑍": "𫭼", - "𡑭": "𡋗", - "𡓁": "𪤄", - "𡓾": "𡋀", - "𡔖": "𡍣", - "𡞵": "㛟", - "𡟫": "𫝪", - "𡠹": "㛿", - "𡢃": "㛠", - "𡮉": "𡭜", - "𡮣": "𡭬", - "𡳳": "𡳃", - "𡸗": "𪨩", - "𡹬": "𪨹", - "𡻕": "岁", - "𡽗": "𡸃", - "𡾱": "㟜", - "𡿖": "𪩛", - "𢍰": "𪪴", - "𢠼": "𢙑", - "𢣐": "𪬚", - "𢣚": "𢘝", - "𢣭": "𢘞", - "𢤩": "𪫡", - "𢤱": "𢘙", - "𢤿": "𪬯", - "𢯷": "𪭝", - "𢶒": "𪭯", - "𢶫": "𢫞", - "𢷮": "𢫊", - "𢹿": "𢬦", - "𢺳": "𪮳", - "𣈶": "暅", - "𣋋": "𣈣", - "𣍐": "𫧃", - "𣙎": "㭣", - "𣜬": "𪳗", - "𣝕": "𣘷", - "𣞻": "𣘓", - "𣠩": "𣞎", - "𣠲": "𣑶", - "𣯩": "𣯣", - "𣯴": "𣭤", - "𣯶": "毶", - "𣽏": "𪶮", - "𣾷": "㳢", - "𣿉": "𣶫", - "𤁣": "𣺽", - "𤄷": "𪶒", - "𤅶": "𣷷", - "𤑳": "𤎻", - "𤑹": "𪹀", - "𤒎": "𤊀", - "𤒻": "𪹹", - "𤓌": "𪹠", - "𤓎": "𤎺", - "𤓩": "𤊰", - "𤘀": "𪺣", - "𤛮": "𤙯", - "𤛱": "𫞢", - "𤜆": "𪺪", - "𤠮": "𪺸", - "𤢟": "𤝢", - "𤢻": "𢢐", - "𤩂": "𫞧", - "𤪺": "㻘", - "𤫩": "㻏", - "𤬅": "𪼴", - "𤳷": "𪽝", - "𤳸": "𤳄", - "𤷃": "𪽭", - "𤸫": "𤶧", - "𤺔": "𪽴", - "𥊝": "𥅿", - "𥌃": "𥅘", - "𥏝": "𪿊", - "𥕥": "𥐰", - "𥖅": "𥐯", - "𥖲": "𪿞", - "𥗇": "𪿵", - "𥗽": "𬒗", - "𥜐": "𫀓", - "𥜰": "𫀌", - "𥞵": "𥞦", - "𥢢": "䅪", - "𥢶": "𫞷", - "𥢷": "𫀮", - "𥨐": "𥧂", - "𥪂": "𥩺", - "𥯤": "𫁳", - "𥴨": "𫂖", - "𥴼": "𫁺", - "𥵃": "𥱔", - "𥵊": "𥭉", - "𥶽": "𫁱", - "𥸠": "𥮋", - "𥻦": "𫂿", - "𥼽": "𥹥", - "𥽖": "𥺇", - "𥾯": "𫄝", - "𥿊": "𦈈", - "𦀖": "𫄦", - "𦂅": "𦈒", - "𦃄": "𦈗", - "𦃩": "𫄯", - "𦅇": "𫄪", - "𦅈": "𫄵", - "𦆲": "𫟇", - "𦒀": "𫅥", - "𦔖": "𫅼", - "𦘧": "𡳒", - "𦟼": "𫆝", - "𦠅": "𫞅", - "𦡝": "𫆫", - "𦢈": "𣍨", - "𦣎": "𦟗", - "𦧺": "𫇘", - "𦪙": "䑽", - "𦪽": "𦨩", - "𦱌": "𫇪", - "𦾟": "𦶻", - "𧎈": "𧌥", - "𧒯": "𫊹", - "𧔥": "𧒭", - "𧕟": "𧉐", - "𧜗": "䘞", - "𧜵": "䙊", - "𧝞": "䘛", - "𧞫": "𫌋", - "𧟀": "𧝧", - "𧡴": "𫌫", - "𧢄": "𫌬", - "𧦝": "𫍞", - "𧦧": "𫍟", - "𧩕": "𫍭", - "𧩙": "𬣥", - "𧩼": "𫍶", - "𧫝": "𫍺", - "𧬤": "𫍼", - "𧭈": "𫍾", - "𧭹": "𫍐", - "𧳟": "𧳕", - "𧵳": "䞌", - "𧶔": "𧹓", - "𧶧": "䞎", - "𧷎": "𪠀", - "𧸘": "𫎨", - "𧹈": "𪥠", - "𧽯": "𫎸", - "𨂐": "𫏌", - "𨄣": "𨀱", - "𨅍": "𨁴", - "𨆪": "𫏕", - "𨇁": "𧿈", - "𨇞": "𨅫", - "𨇤": "𫏨", - "𨇰": "𫏞", - "𨇽": "𫏑", - "𨈊": "𨂺", - "𨈌": "𨄄", - "𨊰": "䢀", - "𨊸": "䢁", - "𨊻": "𨐆", - "𨋢": "䢂", - "𨌈": "𫐍", - "𨍰": "𫐔", - "𨎌": "𫐋", - "𨎮": "𨐉", - "𨏠": "𨐇", - "𨏥": "𨐊", - "𨞺": "𫟫", - "𨟊": "𫟬", - "𨢿": "𨡙", - "𨣈": "𨡺", - "𨣞": "𨟳", - "𨣧": "𨠨", - "𨤻": "𨤰", - "𨥛": "𨱀", - "𨥟": "𫓫", - "𨦫": "䦀", - "𨧀": "𬭊", - "𨧜": "䦁", - "𨧰": "𫟽", - "𨧱": "𨱊", - "𨨏": "𬭛", - "𨨛": "𫓼", - "𨨢": "𫓽", - "𨩰": "𫟾", - "𨪕": "𫓮", - "𨫒": "𨱐", - "𨬖": "𫔏", - "𨭆": "𬭶", - "𨭎": "𬭳", - "𨭖": "𫔑", - "𨭸": "𫔐", - "𨮂": "𨱕", - "𨮳": "𫔒", - "𨯅": "䥿", - "𨯟": "𫔓", - "𨰃": "𫔉", - "𨰋": "𫓳", - "𨰥": "𫔕", - "𨰲": "𫔃", - "𨲳": "𫔖", - "𨳑": "𨸁", - "𨳕": "𨸀", - "𨴗": "𨸅", - "𨴹": "𫔲", - "𨵩": "𨸆", - "𨵸": "𨸇", - "𨶀": "𨸉", - "𨶏": "𨸊", - "𨶮": "𨸌", - "𨶲": "𨸋", - "𨷲": "𨸎", - "𨼳": "𫔽", - "𨽏": "𨸘", - "𩀨": "𫕚", - "𩅙": "𫕨", - "𩎖": "𫖑", - "𩎢": "𩏾", - "𩏂": "𫖓", - "𩏠": "𫖖", - "𩏪": "𩏽", - "𩏷": "𫃗", - "𩑔": "𫖪", - "𩒎": "𫖭", - "𩓣": "𩖕", - "𩓥": "𫖵", - "𩔑": "𫖷", - "𩔳": "𫖴", - "𩖰": "𫠇", - "𩗀": "𩙦", - "𩗓": "𫗈", - "𩗴": "𫗉", - "𩘀": "𩙩", - "𩘝": "𩙭", - "𩘹": "𩙨", - "𩘺": "𩙬", - "𩙈": "𩙰", - "𩚛": "𩟿", - "𩚥": "𩠀", - "𩚩": "𫗡", - "𩚵": "𩠁", - "𩛆": "𩠂", - "𩛌": "𫗤", - "𩛡": "𫗨", - "𩛩": "𩠃", - "𩜇": "𩠉", - "𩜦": "𩠆", - "𩜵": "𩠊", - "𩝔": "𩠋", - "𩝽": "𫗳", - "𩞄": "𩠎", - "𩞦": "𩠏", - "𩞯": "䭪", - "𩟐": "𩠅", - "𩟗": "𫗚", - "𩠴": "𩠠", - "𩡣": "𩡖", - "𩡺": "𩧦", - "𩢡": "𩧬", - "𩢴": "𩧵", - "𩢸": "𩧳", - "𩢾": "𩧮", - "𩣏": "𩧶", - "𩣑": "䯃", - "𩣫": "𩧸", - "𩣵": "𩧻", - "𩣺": "𩧼", - "𩤊": "𩧩", - "𩤙": "𩨆", - "𩤲": "𩨉", - "𩤸": "𩨅", - "𩥄": "𩨋", - "𩥇": "𩨍", - "𩥉": "𩧱", - "𩥑": "𩨌", - "𩦠": "𫠌", - "𩧆": "𩨐", - "𩭙": "𩬣", - "𩯁": "𫙂", - "𩯳": "𩯒", - "𩰀": "𩬤", - "𩰹": "𩰰", - "𩳤": "𩲒", - "𩴵": "𩴌", - "𩵦": "𫠏", - "𩵩": "𩽺", - "𩵹": "𩽻", - "𩶁": "𫚎", - "𩶘": "䲞", - "𩶰": "𩽿", - "𩶱": "𩽽", - "𩷰": "𩾄", - "𩸃": "𩾅", - "𩸄": "𫚝", - "𩸡": "𫚟", - "𩸦": "𩾆", - "𩻗": "𫚨", - "𩻬": "𫚩", - "𩻮": "𫚘", - "𩼶": "𫚬", - "𩽇": "𩾎", - "𩿅": "𫠖", - "𩿤": "𫛠", - "𩿪": "𪉄", - "𪀖": "𫛧", - "𪀦": "𪉅", - "𪀾": "𪉋", - "𪁈": "𪉉", - "𪁖": "𪉌", - "𪂆": "𪉎", - "𪃍": "𪉐", - "𪃏": "𪉏", - "𪃒": "𫛻", - "𪃧": "𫛹", - "𪄆": "𪉔", - "𪄕": "𪉒", - "𪅂": "𫜂", - "𪆷": "𫛾", - "𪇳": "𪉕", - "𪈼": "𱊜", - "𪉸": "𫜊", - "𪋿": "𫧮", - "𪌭": "𫜓", - "𪍠": "𫜕", - "𪓰": "𫜟", - "𪔵": "𪔭", - "𪘀": "𪚏", - "𪘯": "𪚐", - "𪙏": "𫜯", - "𪟖": "𠛾", - "𪷓": "𣶭", - "𫒡": "𫓷", - "𫜦": "𫜫", -}; -// 数据来源:https://github.com/BYVoid/OpenCC/tree/master/data/dictionary/STCharacters.txt -/** - * 转换文本 - * @param {String} str - 待转换的文本 - * @param {Boolean} toT - 是否转换成繁体 - * @returns {String} - 转换结果 - */ -function tranStr(str, toT) { - var i; - var letter; - var code; - var isChinese; - var src; - var result = ""; - if (toT) { - src = s2tData; - } - else { - src = t2sData; - } - if (typeof str !== "string") { - return str; - } - for (i = 0; i < str.length; i++) { - letter = str.charAt(i); - code = str.charCodeAt(i); - // 根据字符的 Unicode 判断是否为汉字,以提高性能 - isChinese = - (code > 0x3400 && code < 0x9fc3) || (code > 0xf900 && code < 0xfa6a); - if (!isChinese) { - result += letter; - continue; - } - let target = src[letter]; - if (target) { - result += target; - } - else { - result += letter; - } - } - return result; -} -var Chinese = { - s2t: function (str) { - return tranStr(str, true); - }, - t2s: function (str) { - return tranStr(str, false); - }, -}; - -let keywords = [ - "章", - "节", - "回", - "節", - "卷", - "部", - "輯", - "辑", - "話", - "集", - "话", - "篇", - " ", - " ", -]; -let containChars = []; -// let containChars = ["[", "。", ";", ";"]; -let startWithChars = [ - "CHAPTER", - "Chapter", - "序章", - "前言", - "声明", - "写在前面的话", - "后记", - "楔子", - "后序", - "章节目录", - "尾声", - "聲明", - "寫在前面的話", - "後記", - "後序", - "章節目錄", - "尾聲", -]; -const txtToHtml = (text, parserRegex, bookLocation) => { - let lines = text.split("\n"); - if (lines.length === 1) { - lines = text.split("\r"); - } - const htmlParts = []; // Use an array to store HTML parts - let isRefresh = false; - if (bookLocation && bookLocation.refresh) { - isRefresh = true; - } - if (lines.length > 10000 && !isRefresh) { - if (!bookLocation || !bookLocation.text) { - bookLocation = { - text: lines[0], - chapterTitle: "", - chapterDocIndex: 0, - }; - } - // --- Slicing and Title Identification Logic --- - let targetLineIndex = lines.findIndex((item) => { - // Optimization: cleanText called only once here if needed often - return cleanText(item) === cleanText(bookLocation.text); - }); - if (targetLineIndex === -1) { - targetLineIndex = 0; - } - // Slice the lines array - const startIndex = Math.max(targetLineIndex - 1000, 0); - const endIndex = Math.min(targetLineIndex + 1000, lines.length); - const relevantLines = lines.slice(startIndex, endIndex); // Process only the relevant slice - // Identify potential titles within the relevant slice - const titlesInSlice = relevantLines.filter((item) => { - const cleaned = cleanText(item); // Clean once - return cleaned && isTitle(cleaned, parserRegex); - }); - // Create a Set of cleaned titles for fast lookup - const cleanedTitlesSet = new Set(titlesInSlice.map((title) => cleanText(title))); - let targetTitleIndex = titlesInSlice.findIndex((item) => { - // Optimization: cleanText called only once here - return cleanText(item) === cleanText(bookLocation.chapterTitle); - }); - if (targetTitleIndex === -1) { - targetTitleIndex = 0; - } - // --- Prepending Logic (if needed) --- - // This part seems related to chapter indexing, ensure it uses the correct indices based on the slice - if (targetTitleIndex < parseInt(bookLocation.chapterDocIndex || "0") - 1) { - let prependLength = parseInt(bookLocation.chapterDocIndex || "0") - targetTitleIndex; - if (prependLength > 0) { - for (let i = 0; i < prependLength; i++) { - // Push to array instead of concatenating - htmlParts.push(`
Chapter ${i}
`); - } - } - } - // --- Main Loop for Relevant Lines --- - for (const item of relevantLines) { - // Iterate over the sliced array - const cleanedItem = cleanText(item); // Clean once per line - // Use the Set for O(1) average lookup - if (cleanedItem && cleanedTitlesSet.has(cleanedItem)) { - htmlParts.push(`${item}
`); // Push to array - } - } - } - else { - // --- Loop for Full File (if not large or no bookLocation) --- - for (const item of lines) { - const cleanedItem = cleanText(item); // Clean once per line - if (cleanedItem && isTitle(cleanedItem, parserRegex)) { - htmlParts.push(`${item}
`); // Push to array - } - } - } - // Join the array at the end - const finalHtml = htmlParts.join(""); - if (finalHtml) { - return finalHtml; - } - else { - // Fallback if no HTML was generated - return `${text}
`; - } -}; -const cleanText = (str) => { - return str - .trim() - .replace(/(\r\n|\n|\r|\t)/gm, "") - .substring(0, 100) - .split("") - .filter((item) => item !== "=" && item !== "-" && item !== "_" && item !== "+") - .join(""); -}; -const isTitle = (line, parserRegex = "") => { - if (parserRegex) { - return new RegExp(parserRegex).test(line); - } - return (line && - line.length < 40 && - !isContain(line) && - (isStartWithChars(line) || - (line.startsWith("第") && startWithDI(line)) || - (line.startsWith("卷") && startWithJUAN(line)) || - (line.indexOf("第") > -1 && - line.lastIndexOf("第") < 7 && - startWithDI(line.substr(line.indexOf("第")))))); -}; -const isContain = (line) => { - return containChars.filter((item) => line.indexOf(item) > -1).length > 0; -}; -const isStartWithChars = (line) => { - return startWithChars.filter((item) => line.startsWith(item)).length > 0; -}; -const startWithDI = (line) => { - let flag = false; - for (let i = 0; i < keywords.length; i++) { - if (/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c\u96f6]+$/.test(line.substring(1, line.indexOf(keywords[i])).trim()) || - /^\d+$/.test(line.substring(1, line.indexOf(keywords[i])).trim())) { - flag = true; - } - if (flag) - break; - } - return flag; -}; -const startWithJUAN = (line) => { - if (/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c\u96f6]+$/.test(line.substring(1, line.indexOf(" "))) || - /^\d+$/.test(line.substring(1, line.indexOf(" ")))) - return true; - if (/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c\u96f6]+$/.test(line.substring(1, line.indexOf(" "))) || - /^\d+$/.test(line.substring(1, line.indexOf(" ")))) - return true; - if (/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u842c\u96f6]+$/.test(line.substring(1)) || - /^\d+$/.test(line.substring(1))) - return true; - return false; -}; - -let lock = false; -const getBlockElement = (Element) => { - return Array.from(Element.querySelectorAll("h1,h2,h3,h4,h5,h6,p,div,ul,dl,ol,li,dt,dd,pre,blockquote,address,kookitmarker")); -}; -const handleScrollPage = (element, animation, delta, doc, flipToNextPage, flipToPrevPage, isMobile) => __awaiter(void 0, void 0, void 0, function* () { - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - const width = element.clientWidth; - if (animation === "mimical" && isMobile !== "yes") { - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "block"; - if (delta > 0) { - flipToPrevPage(); - } - else if (delta < 0) { - flipToNextPage(); - } - setTimeout(() => { - if (!bookDiv) - return {}; - bookDiv.style.display = "none"; - }, 1000); - } - } - const currentScrollLeft = doc.body.scrollLeft; - const scrollDistance = width + gap; - if (delta > 0) { - // previous page - 计算当前页数并减1 - const currentPage = Math.round(currentScrollLeft / scrollDistance); - const targetPage = Math.max(0, currentPage - 1); - const targetScrollLeft = targetPage * scrollDistance; - doc.body.scrollTo({ - top: 0, - left: targetScrollLeft, - behavior: animation === "sliding" && isMobile !== "yes" ? "smooth" : "auto", - }); - } - else if (delta < 0) { - // next page - 计算当前页数并加1 - const currentPage = Math.round(currentScrollLeft / scrollDistance); - const targetPage = currentPage + 1; - const targetScrollLeft = targetPage * scrollDistance; - doc.body.scrollTo({ - top: 0, - left: targetScrollLeft, - behavior: animation === "sliding" && isMobile !== "yes" ? "smooth" : "auto", - }); - } -}); -const findValidChapter = (chapterDocIndex, chapterHref, chapterDocList, flag) => { - let currentChapterIndex = _.findLastIndex(chapterDocList, (chapter) => { - return (chapter.href === chapterHref || - (chapter.href && - chapter.href.includes("#") && - chapter.href.includes(chapterHref))); - }); - if (chapterHref && - _.findLastIndex(chapterDocList, (chapter) => { - return (chapter.href === chapterHref || - (chapter.href && - chapter.href.includes("#") && - chapter.href.includes(chapterHref))); - }) > -1) ; - else { - currentChapterIndex = chapterDocIndex; - } - if (flag === "prev") { - return Object.assign(Object.assign({}, chapterDocList[currentChapterIndex - 1]), { index: currentChapterIndex - 1 }); - } - else { - return Object.assign(Object.assign({}, chapterDocList[currentChapterIndex + 1]), { index: currentChapterIndex + 1 }); - } -}; -const handlePrevChapter = (element, flattenChapters, chapterDocList, readerMode, format, tempLocation, doc, iframe) => __awaiter(void 0, void 0, void 0, function* () { - let chapterDocIndex = parseInt(tempLocation.chapterDocIndex || "0"); - let chapterHref = tempLocation.chapterHref || ""; - if (chapterDocIndex === 0) { - return; - } - let prevChapter = findValidChapter(chapterDocIndex, chapterHref, chapterDocList, "prev"); - if (!prevChapter) - return; - tempLocation.text = "prevChapter"; - tempLocation.page = ""; - yield handleRenderChapter(prevChapter.index, prevChapter.label, prevChapter.href, chapterDocList, element, readerMode, format, tempLocation, doc, iframe); -}); -const handleRenderChapter = (chapterDocIndex, chapterTitle, chapterHref, chapterDocList, element, readerMode, format, tempLocation, doc, iframe) => __awaiter(void 0, void 0, void 0, function* () { - doc.body.innerHTML = ""; - iframe.height = 0 + "px"; - doc.body.scrollTo(0, 0); - if ((chapterTitle && !chapterDocIndex) || - (chapterDocList[chapterDocIndex] && - chapterDocList[chapterDocIndex].label && - chapterTitle && - chapterTitle !== chapterDocList[chapterDocIndex].label && - chapterHref.indexOf("#") === -1)) { - let tempChapterDocIndex = _.findLastIndex(chapterDocList, { - label: chapterTitle, - }); - if (tempChapterDocIndex !== -1) { - chapterDocIndex = tempChapterDocIndex; - } - } - if (chapterDocIndex === -1 && chapterHref.indexOf("#") > -1) { - let href = chapterHref.split("#")[0]; - let tempChapterDocIndex = _.findLastIndex(chapterDocList, (chapter) => { - return (chapter.href === href || - (chapter.href && - chapter.href.includes("#") && - chapter.href.includes(href))); - }); - if (tempChapterDocIndex !== -1) { - chapterDocIndex = tempChapterDocIndex; - } - } - if (chapterDocIndex === -1 || chapterDocIndex > chapterDocList.length - 1) { - chapterDocIndex = 0; - } - let chapterText = yield handleOneChapterDoc(chapterDocList[chapterDocIndex].text, false); - let bodyAttrs = getBodyAttributes(chapterText); - //get viewport width from chapterText - doc.body.innerHTML = chapterText; - if (bodyAttrs["style"]) { - doc.body.setAttribute("style", doc.body.getAttribute("style") || ""); - } - else if (bodyAttrs["class"]) { - doc.body.setAttribute("class", bodyAttrs["class"]); - } - else if (bodyAttrs["id"]) { - doc.body.setAttribute("id", bodyAttrs["id"]); - } - else if (!bodyAttrs["class"]) { - doc.body.removeAttribute("class"); - } - else if (!bodyAttrs["id"]) { - doc.body.removeAttribute("id"); - } - yield handleCssLink(doc); - tempLocation.chapterTitle = chapterTitle; - tempLocation.chapterHref = chapterHref; - tempLocation.chapterDocIndex = chapterDocIndex + ""; - tempLocation.percentage = - chapterDocList - .slice(0, chapterDocIndex) - .map((item) => (item.text ? item.text.size || 1 : 1)) - .reduce((a, b) => a + b, 0) / - chapterDocList - .map((item) => (item.text ? item.text.size || 1 : 1)) - .reduce((a, b) => a + b, 0) + - ""; - tempLocation.text = ""; - yield handleIframeHeight(element, readerMode, format, iframe, doc); - yield handleScrollPosition(element, readerMode, "", "", "", "", doc); -}); -function getBodyAttributes(htmlStr) { - // 匹配 开始标签(忽略大小写) - const bodyTagMatch = htmlStr.match(/]*)>/i); - if (!bodyTagMatch) - return {}; - // 提取属性字符串(如 'id="main" class=dark') - const attrStr = bodyTagMatch[1]; - const attributes = {}; - // 匹配属性键值对 - const attrRegex = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^>\s]+))/g; - let match; - while ((match = attrRegex.exec(attrStr)) !== null) { - const value = match[2] || match[3] || match[4] || ""; - attributes[match[1]] = value; - } - return attributes; -} -const handleCssLink = (doc) => __awaiter(void 0, void 0, void 0, function* () { - let linkList = Array.from(doc.getElementsByTagName("link")); - if (linkList.length === 0) { - return; - } - let styleSheetPromises = []; - for (let index = 0; index < linkList.length; index++) { - const link = linkList[index]; - if (!link.href.endsWith("null")) { - styleSheetPromises.push(new Promise((resolve, reject) => { - link.addEventListener("load", resolve); - })); - } - } - try { - yield Promise.race([ - Promise.all(styleSheetPromises), - new Promise((resolve, reject) => { - setTimeout(() => { - // reject(new Error("Timeout")); - resolve("css load timeout"); - }, 10); - }), - ]); - } - catch (err) { - console.error(err); - } -}); -const handleScrollPosition = (element, readerMode, text, count, href, page, doc) => __awaiter(void 0, void 0, void 0, function* () { - let left = 0; - let targetNode = doc.body; - if (page && readerMode !== "scroll") { - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - const width = convertComputedNum(getComputedStyle(element).width); - let pageWidth = width + gap; - left = pageWidth * (parseInt(page) - 1); - } - else if (text) { - let nodeList = getBlockElement(doc.body); - let targetNodeList = nodeList.filter((s, index) => { - return (cleanText(s.textContent) && - (cleanText(s.textContent) === cleanText(text) || - cleanText(s.textContent) === - Chinese.t2s(cleanText(text)) || - cleanText(s.textContent) === - Chinese.s2t(cleanText(text))) && - (Math.abs(index - parseInt(count)) < 2 || - count === "search" || - count === "ignore" || - count === "next")); - }); - if (targetNodeList.length === 0) { - return; - } - targetNode = getCloestBlock(targetNodeList[0], element, readerMode); - left = targetNode - ? convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)) - : text === "prevChapter" - ? doc.body.scrollWidth - : 0; - } - else if (href && href.indexOf("#") > -1) { - let id = CSS.escape(href.split("#").reverse()[0]); - if (!doc.body.querySelector("#" + id)) { - return; - } - targetNode = getCloestBlock(doc.body.querySelector("#" + id) || doc.body, element, readerMode); - left = targetNode - ? convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)) - : 0; - } - if (readerMode !== "scroll") { - doc.body.scrollTo(left, 0); - } - else { - targetNode.scrollIntoView(); - } -}); -const getCloestBlock = (targetNode, element, readerMode) => { - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let offsetLeft = convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)); - if (readerMode === "scroll") { - return targetNode; - } - else if (readerMode !== "scroll" && - checkDivisibleInRange(parseInt(offsetLeft + ""), (element.clientWidth + gap) / 2)) { - return targetNode; - } - else if (targetNode.parentElement) { - return getCloestBlock(targetNode.parentElement, element, readerMode); - } - else { - return targetNode; - } -}; -const checkDivisibleInRange = (x, y) => { - for (let i = x - 10; i <= x + 10; i++) { - if (i % y === 0) { - return true; - } - } - return false; -}; -const handleRecord = (element, readerMode, flattenChapters, chapterDocList, tempLocation, doc, targetNode) => __awaiter(void 0, void 0, void 0, function* () { - var _a, _b; - if (lock) - return; - let nodeList = getBlockElement(doc.body); - let visibleNode = nodeList.filter((s) => isScrolledIntoView(element, s, readerMode) && - (s.textContent || "").trim()); - let firstVisibleNode = visibleNode[0]; - if (targetNode) { - firstVisibleNode = targetNode; - } - let count = 0; - for (let i = 0; i < nodeList.length; i++) { - if (isScrolledIntoView(element, nodeList[i], readerMode) && - firstVisibleNode && - nodeList[i].innerHTML === firstVisibleNode.innerHTML) { - count = i; - break; - } - } - handleHashChapter(visibleNode, flattenChapters, tempLocation); - if (firstVisibleNode && - !isCurrentNodeFarFromParrent(firstVisibleNode, element, readerMode)) { - tempLocation.text = firstVisibleNode.textContent || ""; - tempLocation.count = count + ""; - tempLocation.page = ""; - let totalSize = chapterDocList - .map((item) => (item.text ? item.text.size || 1 : 1)) - .reduce((a, b) => a + b, 0); - tempLocation.percentage = - chapterDocList - .slice(0, parseInt(tempLocation.chapterDocIndex)) - .map((item) => (item.text ? item.text.size || 1 : 1)) - .reduce((a, b) => a + b, 0) / - totalSize + - ((((_a = chapterDocList.find((_item, index) => index === parseInt(tempLocation.chapterDocIndex))) === null || _a === void 0 ? void 0 : _a.text.size) || 0) / - totalSize) * - (count / nodeList.length) + - ""; - } - else { - tempLocation.page = - ((_b = (yield progressInfo(readerMode, doc, element))) === null || _b === void 0 ? void 0 : _b.currentPage) + ""; - } - lock = true; - setTimeout(() => { - lock = false; - }, 100); -}); -const isCurrentNodeFarFromParrent = (targetNode, element, readerMode) => { - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - if (Math.abs(targetNode.offsetLeft - - getCloestBlock(targetNode, element, readerMode).offsetLeft) > - (element.clientWidth + gap) / 2) { - return true; - } - else { - return false; - } -}; -const handleHashChapter = (visibleNode, flattenChapters, tempLocation) => { - let chapterHref = tempLocation.chapterHref || ""; - let lastIndexOfHash = chapterHref.lastIndexOf("#"); - let beforeHash = ""; - if (lastIndexOfHash === -1) { - beforeHash = chapterHref; - } - else { - beforeHash = chapterHref.substring(0, lastIndexOfHash); - } - for (let index = 0; index < visibleNode.length; index++) { - const element = visibleNode[index]; - if (element.id) { - let newHref = beforeHash + "#" + element.id; - let newIndex = _.findLastIndex(flattenChapters, { - href: newHref, - }); - if (newIndex > -1) { - tempLocation.chapterHref = newHref; - tempLocation.chapterTitle = flattenChapters[newIndex].label; - } - } - } -}; -const handleNextChapter = (element, flattenChapters, chapterDocList, readerMode, format, tempLocation, doc, iframe) => __awaiter(void 0, void 0, void 0, function* () { - let chapterDocIndex = parseInt(tempLocation.chapterDocIndex || "0"); - let chapterHref = tempLocation.chapterHref || ""; - if (chapterDocIndex >= chapterDocList.length - 1) { - tempLocation.percentage = "1"; - return; - } - let nextChapter = findValidChapter(chapterDocIndex, chapterHref, chapterDocList, "next"); - if (!nextChapter) - return; - tempLocation.page = ""; - yield handleRenderChapter(nextChapter.index, nextChapter.label, nextChapter.href, chapterDocList, element, readerMode, format, tempLocation, doc, iframe); -}); -const getAudioText = (element, readerMode, doc) => { - let nodeList = getBlockElement(doc.body).filter((item) => !isParentBlock(item)); - let audioNode = nodeList.filter((s) => { - // 检查文本内容是否存在且不为空 - if (!(s.textContent || "").trim()) { - return false; - } - // 检查是否有父级块元素(排除body) - let parent = s.parentElement; - while (parent && parent !== doc.body) { - // 如果父级元素也在nodeList中,说明当前元素是嵌套的 - if (nodeList.includes(parent)) { - return false; - } - parent = parent.parentElement; - } - return true; - }); - let audioText = audioNode - .filter((item) => { var _a; return item.textContent !== "img" && !((_a = item.textContent) === null || _a === void 0 ? void 0 : _a.startsWith("img")); }) - .map((item) => item.textContent); - let firstSliceIndex = 0; - let visibleText = getVisibleText(element, readerMode, doc); - if (visibleText && visibleText.length > 0) { - let firstVisibleText = visibleText[0]; - firstSliceIndex = audioText.indexOf(firstVisibleText); - } - return audioText.slice(firstSliceIndex); -}; -const getVisibleText = (element, readerMode, doc) => { - let nodeList = getBlockElement(doc.body).filter((item) => !isParentBlock(item)); - let visibleNode = nodeList.filter((s) => isScrolledIntoView(element, s, readerMode) && - (s.textContent || "").trim()); - visibleNode = visibleNode.filter((s) => { - // 检查文本内容是否存在且不为空 - if (!(s.textContent || "").trim()) { - return false; - } - // 检查是否有父级块元素(排除body) - let parent = s.parentElement; - while (parent && parent !== doc.body) { - // 如果父级元素也在nodeList中,说明当前元素是嵌套的 - if (nodeList.includes(parent)) { - return false; - } - parent = parent.parentElement; - } - return true; - }); - return visibleNode - .filter((item) => { var _a; return item.textContent !== "img" && !((_a = item.textContent) === null || _a === void 0 ? void 0 : _a.startsWith("img")); }) - .map((item) => item.textContent); -}; -const handleHighlightSearchNode = (text, style, doc) => { - // First remove any existing highlights - const existingHighlights = doc.querySelectorAll(`span[data-highlight="true"]`); - existingHighlights.forEach((highlight) => { - const parent = highlight.parentNode; - if (parent) { - parent.replaceChild(doc.createTextNode(highlight.textContent || ""), highlight); - } - }); - if (!text.trim()) - return; - // Get block elements and find those containing the target text - let nodeList = Array.from(doc.body.querySelectorAll("span, p, div, h1, h2, h3, h4, h5, h6 ")); - let nodes = nodeList.filter((node) => { - const content = node.textContent || ""; - return content.trim() && content.indexOf(text) > -1; - }); - // For the first matching node, highlight the text - if (nodes.length > 0) { - // Function to process text nodes - const processNode = (node) => { - var _a; - if (node.nodeType === Node.TEXT_NODE) { - const content = node.textContent || ""; - const index = content.indexOf(text); - if (index > -1) { - // Split the text node and insert the highlight - const before = content.substring(0, index); - const after = content.substring(index + text.length); - // Create span with the specified style - const highlightSpan = doc.createElement("span"); - highlightSpan.setAttribute("style", style); - highlightSpan.setAttribute("data-highlight", "true"); - highlightSpan.textContent = text; - // Replace the original text node with three new nodes - const fragment = doc.createDocumentFragment(); - if (before) - fragment.appendChild(doc.createTextNode(before)); - fragment.appendChild(highlightSpan); - if (after) - fragment.appendChild(doc.createTextNode(after)); - (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, node); - return true; // Text was found and highlighted - } - } - return false; // No match in this node - }; - // Process all child nodes recursively until we find a match - const walkAndProcess = (node) => { - if (processNode(node)) - return true; - // Process children if this node didn't contain the text - const childNodes = Array.from(node.childNodes); - for (const child of childNodes) { - if (walkAndProcess(child)) - return true; - } - return false; - }; - for (let i = 0; i < nodes.length; i++) { - walkAndProcess(nodes[i]); - } - } -}; -const handleHighlightAudioNode = (text, style, doc, element, readerMode) => { - // First remove any existing highlights - const existingHighlights = doc.querySelectorAll(`span[data-highlight="true"]`); - existingHighlights.forEach((highlight) => { - const parent = highlight.parentNode; - if (parent) { - parent.replaceChild(doc.createTextNode(highlight.textContent || ""), highlight); - } - }); - if (!text.trim()) - return; - // Get block elements and find those containing the target text - let nodeList = getBlockElement(doc.body).filter((s) => isScrolledIntoView(element, s, readerMode) && - (s.textContent || "").trim()); - let nodes = nodeList.filter((node) => { - const content = node.textContent || ""; - return content.trim() && content.indexOf(text) > -1; - }); - // For the first matching node, highlight the text - if (nodes.length > 0) { - // Function to process text nodes - const processNode = (node) => { - var _a; - if (node.nodeType === Node.TEXT_NODE) { - const content = node.textContent || ""; - const index = content.indexOf(text); - if (index > -1) { - // Split the text node and insert the highlight - const before = content.substring(0, index); - const after = content.substring(index + text.length); - // Create span with the specified style - const highlightSpan = doc.createElement("span"); - highlightSpan.setAttribute("style", style); - highlightSpan.setAttribute("data-highlight", "true"); - highlightSpan.textContent = text; - // Replace the original text node with three new nodes - const fragment = doc.createDocumentFragment(); - if (before) - fragment.appendChild(doc.createTextNode(before)); - fragment.appendChild(highlightSpan); - if (after) - fragment.appendChild(doc.createTextNode(after)); - (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, node); - return true; // Text was found and highlighted - } - } - return false; // No match in this node - }; - // Process all child nodes recursively until we find a match - const walkAndProcess = (node) => { - if (processNode(node)) - return true; - // Process children if this node didn't contain the text - const childNodes = Array.from(node.childNodes); - for (const child of childNodes) { - if (walkAndProcess(child)) - return true; - } - return false; - }; - walkAndProcess(nodes[0]); - } -}; -const getSearchResult = (keyword, chapterDocList) => __awaiter(void 0, void 0, void 0, function* () { - var _d; - let searchResult = []; - for (let i = 0; i < chapterDocList.length; i++) { - let chapterDoc = new DOMParser().parseFromString(yield handleOneChapterDoc(chapterDocList[i].text, true), "text/html"); - let nodeList = getBlockElement(chapterDoc.body).filter((item) => !isParentBlock(item)); - for (let j = 0; j < nodeList.length; j++) { - let keyWordIndex = (nodeList[j].textContent || "").indexOf(keyword); - if (keyWordIndex > -1) { - searchResult.push({ - excerpt: ((_d = nodeList[j].textContent) === null || _d === void 0 ? void 0 : _d.substring(keyWordIndex - 100, keyWordIndex + 100)) || "", - cfi: JSON.stringify({ - text: nodeList[j].textContent, - chapterTitle: chapterDocList[i].label, - chapterDocIndex: i, - chapterHref: chapterDocList[i].href, - count: "search", - percentage: i / chapterDocList.length, - keyword: keyword, - }), - }); - } - } - } - return _.uniq(searchResult, "excerpt"); -}); -const isParentBlock = (myDiv) => { - var children = myDiv.children; - let flag = false; - var blockRegex = /^(address|kookitmarker|section|blockquote|body|center|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i; - let blockElementList = Array.from(children).filter((item) => blockRegex.test(item.nodeName)); - // some elements might contain image and image subtitle - if (blockElementList.length < 3) { - return false; - } - for (var i = 0; i < children.length; i++) { - if (blockRegex.test(children[i].nodeName)) { - flag = true; - break; - } - } - return flag; -}; -const isScrolledIntoView = (element, el, readerMode) => { - var isVisible = false; - var rect = el.getBoundingClientRect(); - if (readerMode !== "scroll" && el.textContent && el.textContent.trim()) { - let elemLeft = rect.left; - isVisible = elemLeft > -10 && elemLeft <= element.clientWidth; - } - else if (readerMode === "scroll" && - el.textContent && - el.textContent.trim()) { - let elemTop = rect.top; - isVisible = - elemTop >= element.scrollTop && - elemTop <= element.scrollTop + element.clientHeight; - } - else if (readerMode !== "scroll") { - let elemLeft = rect.left; - isVisible = elemLeft >= 0 && elemLeft <= element.clientWidth; - } - return isVisible; -}; - -class EventEmitter { - constructor() { - this.callbacks = {}; - this.callbacks.base = {}; - } - /** - * On - */ - on(_names, callback) { - const that = this; - // Errors - if (typeof _names === "undefined" || _names === "") { - console.warn("wrong names"); - return false; - } - if (typeof callback === "undefined") { - console.warn("wrong callback"); - return false; - } - // Resolve names - const names = this.resolveNames(_names); - // Each name - names.forEach(function (_name) { - // Resolve name - const name = that.resolveName(_name); - // Create namespace if not exist - if (!(that.callbacks[name.namespace] instanceof Object)) - that.callbacks[name.namespace] = {}; - // Create callback if not exist - if (!(that.callbacks[name.namespace][name.value] instanceof Array)) - that.callbacks[name.namespace][name.value] = []; - // Add callback - that.callbacks[name.namespace][name.value].push(callback); - }); - return this; - } - /** - * Off - */ - off(_names) { - const that = this; - // Errors - if (typeof _names === "undefined" || _names === "") { - console.warn("wrong name"); - return false; - } - // Resolve names - const names = this.resolveNames(_names); - // Each name - names.forEach(function (_name) { - // Resolve name - const name = that.resolveName(_name); - // Remove namespace - if (name.namespace !== "base" && name.value === "") { - delete that.callbacks[name.namespace]; - } - // Remove specific callback in namespace - else { - // Default - if (name.namespace === "base") { - // Try to remove from each namespace - for (const namespace in that.callbacks) { - if (that.callbacks[namespace] instanceof Object && - that.callbacks[namespace][name.value] instanceof Array) { - delete that.callbacks[namespace][name.value]; - // Remove namespace if empty - if (Object.keys(that.callbacks[namespace]).length === 0) - delete that.callbacks[namespace]; - } - } - } - // Specified namespace - else if (that.callbacks[name.namespace] instanceof Object && - that.callbacks[name.namespace][name.value] instanceof Array) { - delete that.callbacks[name.namespace][name.value]; - // Remove namespace if empty - if (Object.keys(that.callbacks[name.namespace]).length === 0) - delete that.callbacks[name.namespace]; - } - } - }); - return this; - } - /** - * Trigger - */ - trigger(_name, _args = []) { - // Errors - if (typeof _name === "undefined" || _name === "") { - console.warn("wrong name"); - return false; - } - const that = this; - let finalResult = null; - // Default args - const args = !(_args instanceof Array) ? [] : _args; - // Resolve names (should on have one event) - let name = this.resolveNames(_name); - // Resolve name - name = this.resolveName(name[0]); - setTimeout(() => { - if (name.namespace === "base") { - // Try to find callback in each namespace - for (const namespace in that.callbacks) { - if (that.callbacks[namespace] instanceof Object && - that.callbacks[namespace][name.value] instanceof Array && - that.callbacks[namespace][name.value]) { - that.callbacks[namespace][name.value].forEach(function (callback) { - callback.apply(that, args); - }); - } - else if (this.callbacks[name.namespace] instanceof Object && - that.callbacks[name.namespace][name.value]) { - if (name.value === "") { - console.warn("wrong name"); - return this; - } - that.callbacks[name.namespace][name.value].forEach(function (callback) { - callback.apply(that, args); - }); - } - return finalResult; - } - } - }, 100); - // Default namespace - // Specified namespace - } - /** - * Resolve names - */ - resolveNames(_names) { - let names = _names; - names = names.replace(/[^a-zA-Z0-9 ,/.]/g, ""); - names = names.replace(/[,/]+/g, " "); - names = names.split(" "); - return names; - } - /** - * Resolve name - */ - resolveName(name) { - const newName = {}; - const parts = name.split("."); - newName.original = name; - newName.value = parts[0]; - newName.namespace = "base"; // Base namespace - // Specified namespace - if (parts.length > 1 && parts[1] !== "") { - newName.namespace = parts[1]; - } - return newName; - } -} - -/** - * @see https://github.com/fread-ink/epub-cfi-resolver - * @latest a0d7e4e39d5b4adc9150e006e0b6d7af9513ae27 - */ -const ELEMENT_NODE = Node.ELEMENT_NODE; -const TEXT_NODE = Node.TEXT_NODE; -const CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE; -function cfiEscape(str) { - return str.replace(/[\[\]\^,();]/g, `^$&`); -} -// Get indices of all matches of regExp in str -// if `add` is non-null, add it to the matched indices -function matchAll(str, regExp, add) { - add = add || 0; - const matches = []; - let offset = 0; - let m; - do { - m = str.match(regExp); - if (!m) - break; - matches.push(m.index + add); - // @ts-ignore - offset += m.index + m.length; - // @ts-ignore - str = str.slice(m.index + m.length); - } while (offset < str.length); - return matches; -} -// Get the number in a that has the smallest diff to n -function closest(a, n) { - let minDiff; - let closest; - let i, diff; - for (i = 0; i < a.length; i++) { - diff = Math.abs(a[i] - n); - // @ts-ignore - if (!i || diff < minDiff) { - diff = minDiff; - closest = a[i]; - } - } - return closest; -} -// Given a set of nodes that are all children -// and a reference to one of those nodes -// calculate the count/index of the node -// according to the CFI spec. -// Also re-calculate offset if supplied and relevant -function calcSiblingCount(nodes, n, offset) { - let count = 0; - let lastWasElement; - let prevOffset = 0; - let firstNode = true; - let i, node; - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - // @ts-ignore - if (node.nodeType === ELEMENT_NODE) { - if (lastWasElement || firstNode) { - count += 2; - firstNode = false; - } - else { - count++; - } - // @ts-ignore - if (n === node) { - // @ts-ignore - if (node.tagName.toLowerCase() === `img`) { - return { count, offset }; - } - else { - return { count }; - } - } - prevOffset = 0; - lastWasElement = true; - } - else if ((node === null || node === void 0 ? void 0 : node.nodeType) === TEXT_NODE || - (node === null || node === void 0 ? void 0 : node.nodeType) === CDATA_SECTION_NODE) { - if (lastWasElement || firstNode) { - count++; - firstNode = false; - } - // @ts-ignore - if (n === node) { - return { count, offset: offset + prevOffset }; - } - // @ts-ignore - prevOffset += node.textContent.length; - lastWasElement = false; - } - else { - continue; - } - } - throw new Error(`The specified node was not found in the array of siblings`); -} -function compareTemporal(a, b) { - const isA = typeof a === `number`; - const isB = typeof b === `number`; - if (!isA && !isB) - return 0; - if (!isA && isB) - return -1; - if (isA && !isB) - return 1; - return (a || 0.0) - (b || 0.0); -} -function compareSpatial(a, b) { - if (!a && !b) - return 0; - if (!a && b) - return -1; - if (a && !b) - return 1; - const diff = (a.y || 0) - (b.y || 0); - if (diff) - return diff; - return (a.x || 0) - (b.x || 0); -} -class CFI { - constructor(str, opts) { - this.isRange = false; - this.opts = Object.assign({ - // If CFI is a Simple Range, pretend it isn't - // by parsing only the start of the range - flattenRange: false, - // Strip temporal, spatial, offset and textLocationAssertion - // from places where they don't make sense - stricter: true, - }, opts || {}); - this.cfi = str; - this.parts = []; - const isCFI = /^epubcfi\((.*)\)$/; - str = str.trim(); - const m = str.match(isCFI); - if (!m) - throw new Error(`Not a valid CFI`); - if (m.length < 2) - return; // Empty CFI - str = m[1] || ``; - let parsed, offset, newDoc; - let subParts = []; - let sawComma = 0; - while (str.length) { - ({ parsed, offset, newDoc } = this.parse(str)); - if (!parsed || offset === null) - throw new Error(`Parsing failed`); - if (sawComma && newDoc) - throw new Error(`CFI is a range that spans multiple documents. This is not allowed`); - subParts.push(parsed); - // Handle end of string - if (newDoc || str.length - offset <= 0) { - // Handle end if this was a range - if (sawComma === 2) { - // @ts-ignore - this.to = subParts; - } - else { - // not a range - this.parts.push(subParts); - } - subParts = []; - } - str = str.slice(offset); - // Handle Simple Ranges - if (str[0] === `,`) { - if (sawComma === 0) { - if (subParts.length) { - this.parts.push(subParts); - } - subParts = []; - } - else if (sawComma === 1) { - if (subParts.length) { - // @ts-ignore - this.from = subParts; - } - subParts = []; - } - str = str.slice(1); - sawComma++; - } - } - // @ts-ignore - if (this.from && this.from.length) { - // @ts-ignore - if (this.opts.flattenRange || !this.to || !this.to.length) { - // @ts-ignore - this.parts = this.parts.concat(this.from); - // @ts-ignore - delete this.from; - // @ts-ignore - delete this.to; - } - else { - this.isRange = true; - } - } - // @ts-ignore - if (this.opts.stricter) { - // @ts-ignore - this.removeIllegalOpts(); - } - } - removeIllegalOpts(parts) { - if (!parts) { - // @ts-ignore - if (this.from) { - // @ts-ignore - this.removeIllegalOpts(this.from); - // @ts-ignore - if (!this.to) - return; - // @ts-ignore - parts = this.to; - } - else { - parts = this.parts; - } - } - let i, j, part, subpart; - for (i = 0; i < parts.length; i++) { - part = parts[i]; - for (j = 0; j < part.length - 1; j++) { - subpart = part[j]; - delete subpart.temporal; - delete subpart.spatial; - delete subpart.offset; - delete subpart.textLocationAssertion; - } - } - } - static generatePart(node, offset, extra) { - let cfi = ``; - let o; - while (node.parentNode) { - // @ts-ignore - o = calcSiblingCount(node.parentNode.childNodes, node, offset); - if (!cfi && o.offset) - cfi = `:` + o.offset; - // @ts-ignore - cfi = - `/` + - o.count + - (node.id ? `[` + cfiEscape(node.id) + `]` : ``) + - cfi; - // debugger - node = node.parentNode; - } - return cfi; - } - static generate(node, offset, extra) { - let cfi; - if (Array.isArray(node)) { - const strs = []; - for (const o of node) { - strs.push(this.generatePart(o.node, o.offset, extra)); - } - cfi = strs.join(`!`); - } - else { - cfi = this.generatePart(node, offset, extra); - } - if (extra) - cfi += extra; - return `epubcfi(` + cfi + `)`; - } - static toParsed(cfi) { - if (cfi.isRange) { - return cfi.getFrom(); - } - else { - return cfi.get(); - } - } - // Takes two CFI paths and compares them - static comparePath(a, b) { - const max = Math.max(a.length, b.length); - let i, cA, cB, diff; - for (i = 0; i < max; i++) { - cA = a[i]; - cB = b[i]; - if (!cA) - return -1; - if (!cB) - return 1; - diff = this.compareParts(cA, cB); - if (diff) - return diff; - } - return 0; - } - // Sort an array of CFI objects - static sort(a) { - // @ts-ignore - a.sort((a, b) => { - return this.compare(a, b); - }); - } - // Takes two CFI objects and compares them. - static compare(a, b) { - let oA = a.get(); - let oB = b.get(); - if (a.isRange || b.isRange) { - if (a.isRange && b.isRange) { - const diff = this.comparePath(oA.from, oB.from); - if (diff) - return diff; - return this.comparePath(oA.to, oB.to); - } - if (a.isRange) - oA = oA.from; - if (b.isRange) - oB = oB.from; - return this.comparePath(oA, oB); - } - else { - // neither a nor b is a range - return this.comparePath(oA, oB); - } - } - // Takes two parsed path parts (assuming path is split on '!') and compares them. - static compareParts(a, b) { - const max = Math.max(a.length, b.length); - let i, cA, cB, diff; - for (i = 0; i < max; i++) { - cA = a[i]; - cB = b[i]; - if (!cA) - return -1; - if (!cB) - return 1; - diff = cA.nodeIndex - cB.nodeIndex; - if (diff) - return diff; - // The paths must be equal if the "before the first node" syntax is used - // and this must be the last subpart (assuming a valid CFI) - if (cA.nodeIndex === 0) { - return 0; - } - // Don't bother comparing offsets, temporals or spatials - // unless we're on the last element, since they're not - // supposed to be on elements other than the last - if (i < max - 1) - continue; - // Only compare spatials or temporals for element nodes - if (cA.nodeIndex % 2 === 0) { - diff = compareTemporal(cA.temporal, cB.temporal); - if (diff) - return diff; - diff = compareSpatial(cA.spatial, cB.spatial); - if (diff) - return diff; - } - diff = (cA.offset || 0) - (cB.offset || 0); - if (diff) - return diff; - } - return 0; - } - decodeEntities(dom, str) { - try { - const el = dom.createElement(`textarea`); - el.innerHTML = str; - return el.value || ``; - } - catch (err) { - // TODO fall back to simpler decode? - // e.g. regex match for stuff like and - return str; - } - } - // decode HTML/XML entities and compute length - trueLength(dom, str) { - return this.decodeEntities(dom, str).length; - } - getFrom() { - if (!this.isRange) - throw new Error(`Trying to get beginning of non-range CFI`); - // @ts-ignore - if (!this.from) { - return this.deepClone(this.parts); - } - const parts = this.deepClone(this.parts); - // @ts-ignore - parts[parts.length - 1] = parts[parts.length - 1].concat(this.from); - return parts; - } - getTo() { - if (!this.isRange) - throw new Error(`Trying to get end of non-range CFI`); - const parts = this.deepClone(this.parts); - // @ts-ignore - parts[parts.length - 1] = parts[parts.length - 1].concat(this.to); - return parts; - } - get() { - if (this.isRange) { - return { - from: this.getFrom(), - to: this.getTo(), - isRange: true, - }; - } - return this.deepClone(this.parts); - } - parseSideBias(o, loc) { - if (!loc) - return; - const m = loc.trim().match(/^(.*);s=([ba])$/); - if (!m || m.length < 3) { - if (typeof o.textLocationAssertion === `object`) { - o.textLocationAssertion.post = loc; - } - else { - o.textLocationAssertion = loc; - } - return; - } - if (m[1]) { - if (typeof o.textLocationAssertion === `object`) { - o.textLocationAssertion.post = m[1]; - } - else { - o.textLocationAssertion = m[1]; - } - } - if (m[2] === `a`) { - o.sideBias = `after`; - } - else { - o.sideBias = `before`; - } - } - parseSpatialRange(range) { - if (!range) - return undefined; - const m = range.trim().match(/^([\d\.]+):([\d\.]+)$/); - if (!m || m.length < 3) - return undefined; - const o = { - x: parseInt(m[1]), - y: parseInt(m[2]), - }; - if (typeof o.x !== `number` || typeof o.y !== `number`) { - return undefined; - } - return o; - } - parse(cfi) { - const o = {}; - const isNumber = /[\d]/; - let f; - let state; - let prevState; - let cur, escape; - let seenColon = false; - let seenSlash = false; - let i; - for (i = 0; i <= cfi.length; i++) { - if (i < cfi.length) { - cur = cfi[i]; - } - else { - cur = ``; - } - if (cur === `^` && !escape) { - escape = true; - continue; - } - if (state === `/`) { - if (cur.match(isNumber)) { - if (!f) { - f = cur; - } - else { - f += cur; - } - escape = false; - continue; - } - else { - if (f) { - // @ts-ignore - o.nodeIndex = parseInt(f); - f = null; - } - prevState = state; - state = null; - } - } - if (state === `:`) { - if (cur.match(isNumber)) { - if (!f) { - f = cur; - } - else { - f += cur; - } - escape = false; - continue; - } - else { - if (f) { - // @ts-ignore - o.offset = parseInt(f); - f = null; - } - prevState = state; - state = null; - } - } - if (state === `@`) { - let done = false; - if (cur.match(isNumber) || cur === `.` || cur === `:`) { - if (cur === `:`) { - if (!seenColon) { - seenColon = true; - } - else { - done = true; - } - } - } - else { - done = true; - } - if (!done) { - if (!f) { - f = cur; - } - else { - f += cur; - } - escape = false; - continue; - } - else { - prevState = state; - state = null; - // @ts-ignore - if (f && seenColon) - o.spatial = this.parseSpatialRange(f); - f = null; - } - } - if (state === `~`) { - if (cur.match(isNumber) || cur === `.`) { - if (!f) { - f = cur; - } - else { - f += cur; - } - escape = false; - continue; - } - else { - if (f) { - // @ts-ignore - o.temporal = parseFloat(f); - } - prevState = state; - state = null; - f = null; - } - } - if (!state) { - if (cur === `!`) { - i++; - state = cur; - break; - } - if (cur === `,`) { - break; - } - if (cur === `/`) { - if (seenSlash) { - break; - } - else { - seenSlash = true; - prevState = state; - state = cur; - escape = false; - continue; - } - } - if (cur === `:` || cur === `~` || cur === `@`) { - // @ts-ignore - if (this.opts.stricter) { - // We've already had a temporal or spatial indicator - // and offset does not make sense and the same time - // @ts-ignore - if (cur === `:` && - (typeof o.temporal !== `undefined` || - typeof o.spatial !== `undefined`)) { - break; - } - // We've already had an offset - // and temporal or spatial do not make sense at the same time - // @ts-ignore - if ((cur === `~` || cur === `@`) && - typeof o.offset !== `undefined`) { - break; - } - } - prevState = state; - state = cur; - escape = false; - seenColon = false; // only relevant for '@' - continue; - } - if (cur === `[` && !escape && prevState === `:`) { - prevState = state; - state = `[`; - escape = false; - continue; - } - if (cur === `[` && !escape && prevState === `/`) { - prevState = state; - state = `nodeID`; - escape = false; - continue; - } - } - if (state === `[`) { - if (cur === `]` && !escape) { - prevState = state; - state = null; - this.parseSideBias(o, f); - f = null; - } - else if (cur === `,` && !escape) { - // @ts-ignore - o.textLocationAssertion = {}; - if (f) { - // @ts-ignore - o.textLocationAssertion.pre = f; - } - f = null; - } - else { - if (!f) { - f = cur; - } - else { - f += cur; - } - } - escape = false; - continue; - } - if (state === `nodeID`) { - if (cur === `]` && !escape) { - prevState = state; - state = null; - // @ts-ignore - o.nodeID = f; - f = null; - } - else { - if (!f) { - f = cur; - } - else { - f += cur; - } - } - escape = false; - continue; - } - escape = false; - } - // @ts-ignore - if (!o.nodeIndex && o.nodeIndex !== 0) - throw new Error(`Missing child node index in CFI`); - return { parsed: o, offset: i, newDoc: state === `!` }; - } - // The CFI counts child nodes differently from the DOM - // Retrieve the child of parentNode at the specified index - // according to the CFI standard way of counting - getChildNodeByCFIIndex(dom, parentNode, index, offset) { - // console.log(`getChildNodeByCFIIndex`, { parentNode, index, offset }) - const children = parentNode.childNodes; - if (!children.length) - return { node: parentNode, offset: 0 }; - // index is pointing to the virtual node before the first node - // as defined in the CFI spec - if (index <= 0) { - return { node: children[0], relativeToNode: `before`, offset: 0 }; - } - let cfiCount = 0; - let lastChild; - let i, child; - // console.log(children, children.length) - for (i = 0; i < children.length; i++) { - child = children[i]; - // @ts-ignore - switch (child.nodeType) { - case ELEMENT_NODE: - // If the previous node was also an element node - // then we have to pretend there was a text node in between - // the current and previous nodes (according to the CFI spec) - // so we increment cfiCount by two - if (cfiCount % 2 === 0) { - cfiCount += 2; - if (cfiCount >= index) { - // @ts-ignore - if (child.tagName.toLowerCase() === `img` && offset) { - return { node: child, offset }; - } - return { node: child, offset: 0 }; - } - } - else { - // Previous node was a text node - cfiCount += 1; - if (cfiCount === index) { - // @ts-ignore - if (child.tagName.toLowerCase() === `img` && offset) { - return { node: child, offset }; - } - return { node: child, offset: 0 }; - // This happens when offset into the previous text node was greater - // than the number of characters in that text node - // So we return a position at the end of the previous text node - } - else if (cfiCount > index) { - if (!lastChild) { - return { node: parentNode, offset: 0 }; - } - // @ts-ignore - return { - node: lastChild, - offset: this.trueLength(dom, lastChild.textContent), - }; - } - } - lastChild = child; - break; - case TEXT_NODE: - case CDATA_SECTION_NODE: - // console.log('TEXT') - // If this is the first node or the previous node was an element node - if (cfiCount === 0 || cfiCount % 2 === 0) { - cfiCount += 1; - } - if (cfiCount === index) { - // If offset is greater than the length of the current text node - // then we assume that the next node will also be a text node - // and that we'll be combining them with the current node - // @ts-ignore - const trueLength = this.trueLength(dom, child.textContent); - if (offset >= trueLength) { - offset -= trueLength; - } - else { - return { node: child, offset: offset }; - } - } - lastChild = child; - break; - default: - continue; - } - } - // console.log(lastChild, index, cfiCount) - // index is pointing to the virtual node after the last child - // as defined in the CFI spec - if (index > cfiCount) { - const o = { relativeToNode: `after`, offset: 0 }; - if (!lastChild) { - // @ts-ignore - o.node = parentNode; - } - else { - // @ts-ignore - o.node = lastChild; - } - // @ts-ignore - if (this.isTextNode(o.node)) { - // @ts-ignore - o.offset = this.trueLength(dom, o.node.textContent.length); - } - return o; - } - } - isTextNode(node) { - if (!node) - return false; - if (node.nodeType === TEXT_NODE || node.nodeType === CDATA_SECTION_NODE) { - return true; - } - return false; - } - // Use a Text Location Assertion to correct and offset - correctOffset(dom, node, offset, assertion) { - let curNode = node; - let matchStr; - if (typeof assertion === `string`) { - matchStr = this.decodeEntities(dom, assertion); - } - else { - assertion.pre = this.decodeEntities(dom, assertion.pre); - assertion.post = this.decodeEntities(dom, assertion.post); - matchStr = assertion.pre + `.` + assertion.post; - } - if (!this.isTextNode(node)) { - return { node, offset: 0 }; - } - // @ts-ignore - while (this.isTextNode(curNode.previousSibling)) { - // @ts-ignore - curNode = curNode.previousSibling; - } - const startNode = curNode; - let str; - const nodeLengths = []; - let txt = ``; - let i = 0; - while (this.isTextNode(curNode)) { - // @ts-ignore - str = this.decodeEntities(dom, curNode.textContent); - nodeLengths[i] = str.length; - txt += str; - if (!curNode.nextSibling) - break; - // @ts-ignore - curNode = curNode.nextSibling; - i++; - } - // Find all matches to the Text Location Assertion - const matchOffset = assertion.pre ? assertion.pre.length : 0; - const m = matchAll(txt, new RegExp(matchStr), matchOffset); - if (!m.length) - return { node, offset }; - // Get the match that has the closest offset to the existing offset - let newOffset = closest(m, offset); - if (curNode === node && newOffset === offset) { - return { node, offset }; - } - i = 0; - curNode = startNode; - // @ts-ignore - while (newOffset >= nodeLengths[i]) { - // @ts-ignore - newOffset -= nodeLengths[i]; - if (newOffset < 0) - return { node, offset }; - const nodeOffsets = []; // added because original code has nodeOffsets undefined. @see https://github.com/fread-ink/epub-cfi-resolver/blob/master/index.js#L826 - if (!curNode.nextSibling || i + 1 >= nodeOffsets.length) - return { node, offset }; - i++; - // @ts-ignore - curNode = curNode.nextSibling; - } - return { node: curNode, offset: newOffset }; - } - resolveNode(index, subparts, dom, opts) { - opts = Object.assign({}, opts || {}); - if (!dom) - throw new Error(`Missing DOM argument`); - // Traverse backwards until a subpart with a valid ID is found - // or the first subpart is reached - let startNode; - if (index === 0) { - startNode = dom.querySelector(`package`); - } - if (!startNode) { - for (const n of dom.childNodes) { - if (n.nodeType === ELEMENT_NODE) { - // if (n.nodeType === Node.DOCUMENT_NODE) { - startNode = n; - break; - } - } - } - // custom - startNode = dom; - // debugger - if (!startNode) - throw new Error(`Document incompatible with CFIs`); - let node = startNode; - let startFrom = 0; - let i; - let subpart; - for (i = subparts.length - 1; i >= 0; i--) { - subpart = subparts[i]; - // @ts-ignore - if (!opts.ignoreIDs && - subpart.nodeID && - (node = dom.getElementById(subpart.nodeID))) { - startFrom = i + 1; - break; - } - } - // console.log(startNode, startFrom) - if (!node) { - node = startNode; - } - let o = { node, offset: 0 }; - for (i = startFrom; i < subparts.length; i++) { - subpart = subparts[i]; - if (subpart) { - // console.log(o, dom, o.node, subpart.nodeIndex, subpart.offset) - // @ts-ignore - o = this.getChildNodeByCFIIndex(dom, o.node, subpart.nodeIndex, subpart.offset); - // @ts-ignore - if (subpart.textLocationAssertion) { - // console.log(subparts, subpart, o) - // @ts-ignore - o = this.correctOffset(dom, o.node, subpart.offset, subpart.textLocationAssertion); - } - } - } - return o; - } - // Each part of a CFI (as separated by '!') - // references a separate HTML/XHTML/XML document. - // This function takes an index specifying the part - // of the CFI and the appropriate Document or XMLDocument - // that is referenced by the specified part of the CFI - // and returns the URI for the document referenced by - // the next part of the CFI - // If the opt `ignoreIDs` is true then IDs - // will not be used while resolving - resolveURI(index, dom, opts) { - opts = opts || {}; - if (index < 0 || index > this.parts.length - 2) { - throw new Error(`index is out of bounds`); - } - const subparts = this.parts[index]; - if (!subparts) - throw new Error(`Missing CFI part for index: ` + index); - // @ts-ignore - const o = this.resolveNode(index, subparts, dom, opts); - // debugger - let node = o.node; - // @ts-ignore - const tagName = node.tagName.toLowerCase(); - if (tagName === `itemref` && - // @ts-ignore - node.parentNode.tagName.toLowerCase() === `spine`) { - // @ts-ignore - const idref = node.getAttribute(`idref`); - if (!idref) - throw new Error(`Referenced node had not 'idref' attribute`); - // @ts-ignore - node = dom.getElementById(idref); - if (!node) - throw new Error(`Specified node is missing from manifest`); - // @ts-ignore - const href = node.getAttribute(`href`); - if (!href) - throw new Error(`Manifest item is missing href attribute`); - return href; - } - if (tagName === `iframe` || tagName === `embed`) { - // @ts-ignore - const src = node.getAttribute(`src`); - if (!src) - throw new Error(tagName + ` element is missing 'src' attribute`); - return src; - } - if (tagName === `object`) { - // @ts-ignore - const data = node.getAttribute(`data`); - if (!data) - throw new Error(tagName + ` element is missing 'data' attribute`); - return data; - } - if (tagName === `image` || tagName === `use`) { - // @ts-ignore - const href = node.getAttribute(`xlink:href`); - if (!href) - throw new Error(tagName + ` element is missing 'xlink:href' attribute`); - return href; - } - throw new Error(`No URI found`); - } - deepClone(o) { - return JSON.parse(JSON.stringify(o)); - } - resolveLocation(dom, parts) { - const index = parts.length - 1; - const subparts = parts[index]; - if (!subparts) - throw new Error(`Missing CFI part for index: ` + index); - // @ts-ignore - const o = this.resolveNode(index, subparts, dom); - // @ts-ignore - const lastPart = this.deepClone(subparts[subparts.length - 1]); - delete lastPart.nodeIndex; - // @ts-ignore - if (!lastPart.offset) - delete o.offset; - return Object.assign(Object.assign({}, lastPart), o); - } - // Takes the Document or XMLDocument for the final - // document referenced by the CFI - // and returns the node and offset into that node - resolveLast(dom, opts) { - opts = Object.assign({ - range: false, - }, opts || {}); - if (!this.isRange) { - return this.resolveLocation(dom, this.parts); - } - // @ts-ignore - if (opts.range) { - const range = dom.createRange(); - const from = this.getFrom(); - if (from.relativeToNode === `before`) { - // @ts-ignore - range.setStartBefore(from.node, from.offset); - } - else if (from.relativeToNode === `after`) { - // @ts-ignore - range.setStartAfter(from.node, from.offset); - } - else { - range.setStart(from.node, from.offset); - } - const to = this.getTo(); - if (to.relativeToNode === `before`) { - // @ts-ignore - range.setEndBefore(to.node, to.offset); - } - else if (to.relativeToNode === `after`) { - // @ts-ignore - range.setEndAfter(to.node, to.offset); - } - else { - range.setEnd(to.node, to.offset); - } - return range; - } - return { - from: this.resolveLocation(dom, this.getFrom()), - to: this.resolveLocation(dom, this.getTo()), - isRange: true, - }; - } - resolve(doc, opts) { - // @ts-ignore - return this.resolveLast(doc, opts); - } -} - -const classes = [ - "color-0", - "color-1", - "color-2", - "color-3", - "line-0", - "line-1", - "line-2", - "line-3", -]; -const colors = ["#FEF3CD", "#FBFACC", "#CEFACD", "#CDE9FA"]; -const lines = ["#FF0000", "#000080", "#0000FF", "#2EFF2E"]; -const pdfColors = ["#fac106", "#ebe702", "#0be603", "#0493e6"]; -const showNoteHighlight = (range, colorIndex, noteKey, handleNoteClick, doc, iframe) => { - var _a, _b; - let colorCode = classes[colorIndex]; - let iWin = iframe.contentWindow || ((_a = iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.defaultView); - let temp = range; - temp = [temp]; - // sleep(500); - let selection = rangy.getSelection(iframe); - selection.restoreCharacterRanges(doc, temp); - let newRange = selection.getRangeAt(0); - highlightRange(newRange, colorCode, noteKey, handleNoteClick, doc); - if (!iWin || !iWin.getSelection()) - return; - (_b = iWin.getSelection()) === null || _b === void 0 ? void 0 : _b.empty(); -}; -const showPDFHighlight = (selected, colorIndex, noteKey, handleNoteClick, page, scale, doc) => { - let colorCode = classes[colorIndex]; - let pageElement = doc.querySelector(".noteLayer"); - let docLayer = doc.querySelector("#koodoPDFLayer"); - var viewport = page.getViewport({ scale: scale }); - let rects = []; - //convertToViewportRectangle - for (let i = 0; i < selected.coords.length; i++) { - const rect = selected.coords[i]; - var bounds = viewport.convertToViewportRectangle(rect); - let width = Math.abs(bounds[0] - bounds[2]); - let height = Math.abs(bounds[1] - bounds[3]); - let top = Math.min(bounds[1], bounds[3]); - let left = Math.min(bounds[0], bounds[2]); - let bottom = top + height; - let right = left + width; - if (Math.abs(height - viewport.height) < 10 || - Math.abs(width - viewport.width) < 10 || - width === 0 || - height === 0) { - continue; - } - rects.push({ width, height, top, left, bottom, right }); - } - //获取最小的高度 - let minHeight = 10000; - rects.forEach((rect) => { - if (rect.height < minHeight) { - minHeight = rect.height; - } - }); - // 按宽度从小到大排序 - const sortedRects = rects.sort((a, b) => a.width - b.width); - // 去除bottom相差小于5且宽度更小的rect,保留宽度最大的rect - const filteredRects = []; - for (let i = 0; i < sortedRects.length; i++) { - const currentRect = sortedRects[i]; - const currentBottom = currentRect.bottom; - // 检查是否有bottom相差小于5且宽度更大的rect - const hasSimilarBottomWithLargerWidth = sortedRects.some((otherRect, otherIndex) => { - if (otherIndex === i) - return false; - const otherBottom = otherRect.bottom; - return (Math.abs(currentBottom - otherBottom) < minHeight && - ((otherRect.left <= currentRect.left && - otherRect.right >= currentRect.right) || - (otherRect.left <= currentRect.left && - Math.abs(otherRect.right - currentRect.right) < 5) || - (Math.abs(otherRect.left - currentRect.left) < 5 && - otherRect.right >= currentRect.right))); - }); - // 如果没有找到bottom相差小于5且宽度更大的rect,则保留当前rect - if (!hasSimilarBottomWithLargerWidth) { - filteredRects.push(currentRect); - } - } - for (let i = 0; i < filteredRects.length; i++) { - const rect = filteredRects[i]; - var newNode = document.createElement("div"); - if (!docLayer) { - continue; - } - newNode === null || newNode === void 0 ? void 0 : newNode.setAttribute("style", "position: absolute;" + - (colorCode.indexOf("color") > -1 - ? "background-color: " - : "border-bottom: ") + - (colorCode.indexOf("color") > -1 - ? pdfColors[colorCode.split("-")[1]] - : `2px solid ${lines[colorCode.split("-")[1]]}`) + - "; left:" + - (rect.left + parseFloat(getComputedStyle(docLayer).marginLeft)) + - "px; top:" + - rect.top + - "px;" + - "width:" + - rect.width + - "px; height:" + - rect.height + - "px; z-index: 1; cursor: pointer; opacity: " + - (colorCode.indexOf("color") > -1 ? 0.3 : 1) + - ";"); - newNode === null || newNode === void 0 ? void 0 : newNode.setAttribute("data-key", noteKey); - newNode === null || newNode === void 0 ? void 0 : newNode.setAttribute("class", "kookit-note"); - newNode === null || newNode === void 0 ? void 0 : newNode.addEventListener("click", (event) => { - if (event && event.target) { - if (event.target.dataset && - event.target.dataset.key) { - handleNoteClick(event); - } - } - }); - newNode.ontouchend = (event) => { - if (window.isSwiping) { - return; - } - if (event && event.target) { - if (event.target.dataset && - event.target.dataset.key) { - handleNoteClick(event); - } - } - event.preventDefault(); - event.stopPropagation(); - }; - pageElement.appendChild(newNode); - } -}; -const clearHighlight = (doc) => { - const elements = doc.querySelectorAll(".kookit-note"); - for (let index = 0; index < elements.length; index++) { - const element = elements[index]; - element.parentNode.removeChild(element); - } -}; -const highlightRange = (range, colorCode, noteKey, handleNoteClick, doc) => { - const rects = range.nativeRange.getClientRects(); - const validRects = []; - // 将rects转换为数组并按宽度从小到大排序 - const sortedRects = Array.from(rects).sort((a, b) => a.width - b.width); - // 获取所有rects中的最大宽度 - const maxWidth = sortedRects.length - ? Math.max(...Array.from(rects).map((rect) => rect.width)) - : 0; - // 过滤重复和无效的矩形 - for (let index = 0; index < sortedRects.length; index++) { - const rect = sortedRects[index]; - // 过滤掉宽度或高度为0的矩形 - if (rect.width <= 0 || rect.height <= 0) { - continue; - } - // 检查是否与已有矩形重叠 - const isOverlapping = validRects.some((validRect) => { - return (Math.abs(rect.bottom - validRect.bottom) < 5 && rect.width === maxWidth); - }); - if (!isOverlapping) { - validRects.push(rect); - } - } - for (let index = 0; index < validRects.length; index++) { - const rect = validRects[index]; - var newNode = document.createElement("span"); - newNode === null || newNode === void 0 ? void 0 : newNode.setAttribute("style", "position: absolute;" + - (colorCode.indexOf("color") > -1 - ? "background-color: " - : "border-bottom: ") + - (colorCode.indexOf("color") > -1 - ? colors[colorCode.split("-")[1]] + ";opacity: 1" - : `2px solid ${lines[colorCode.split("-")[1]]}`) + - ";left:" + - (Math.min(rect.left, rect.x) + doc.body.scrollLeft) + - "px; top:" + - (Math.min(rect.top, rect.y) + doc.body.scrollTop) + - "px;" + - "width:" + - rect.width + - "px; height:" + - rect.height + - "px; z-index:-1;opacity: " + - (colorCode.indexOf("color") > -1 ? 0.8 : 1) + - "; cursor: pointer;"); - newNode.setAttribute("class", " kookit-note"); - newNode.setAttribute("data-key", noteKey); - // newNode.setAttribute("onclick", `window.handleNoteClick()`); - doc.body.appendChild(newNode); - var clickNode = document.createElement("span"); - clickNode === null || clickNode === void 0 ? void 0 : clickNode.setAttribute("style", "position: absolute;" + - "left:" + - (Math.min(rect.left, rect.x) + doc.body.scrollLeft) + - "px; top:" + - (Math.min(rect.top, rect.y) + doc.body.scrollTop) + - "px;" + - "width:" + - rect.width + - "px; height:" + - rect.height + - "px; z-index:1;"); - clickNode.setAttribute("class", " kookit-note"); - clickNode.setAttribute("data-key", noteKey); - clickNode.addEventListener("click", (event) => { - if (event && event.target) { - if (event.target.dataset && - event.target.dataset.key) { - handleNoteClick(event); - } - } - }); - clickNode.ontouchend = (event) => { - if (window.isSwiping) { - return; - } - if (event && event.target) { - if (event.target.dataset && - event.target.dataset.key) { - handleNoteClick(event); - } - } - event.preventDefault(); - event.stopPropagation(); - }; - doc.body.appendChild(clickNode); - } -}; - -function createBookElement(sectionCount) { - let tempDiv = document.getElementById("book"); - if (tempDiv) { - tempDiv.remove(); - } - // Create the book div - const bookDiv = document.createElement("div"); - bookDiv.id = "book"; - // Create the canvas element - const canvas = document.createElement("canvas"); - canvas.id = "pageflip-canvas"; - // Create the pages div - const pagesDiv = document.createElement("div"); - pagesDiv.id = "pages"; - // Create the section elements based on sectionCount - for (let i = 0; i < sectionCount; i++) { - const section = document.createElement("section"); - pagesDiv.appendChild(section); - } - // Append the canvas and pages div to the book div - bookDiv.appendChild(canvas); - bookDiv.appendChild(pagesDiv); - // Append the book div to the body - document.body.appendChild(bookDiv); - let css = ` - #book { - position: fixed; - width: 200vw; - height: 100vh; - left: -100vw; - top: 0vh; - float: left; - margin: 0; - display: none; - } - - #pages section { - display: block; - width: 100vw; - height: 100vh; - position: absolute; - left: 100vw; - top: 0px; - overflow: hidden; - margin: 0; - } - - #pageflip-canvas { - position: absolute; - z-index: 100; - margin: 0; - } - `; - let style = document.createElement("style"); - style.innerHTML = css; - document.head.appendChild(style); -} -const addPageAnimation = (totalPage, isDarkMode, backgroundColor) => { - // Example usage: create a book element with 3 sections - createBookElement(totalPage + 1); - var WINDOW_WIDTH = window.innerWidth; - var WINDOW_HEIGHT = window.innerHeight; - // The canvas size equals to the book dimensions + this padding - var CANVAS_PADDING = 0; - // Dimensions of the whole book - var BOOK_WIDTH = 2 * WINDOW_WIDTH - CANVAS_PADDING; - var BOOK_HEIGHT = WINDOW_HEIGHT - CANVAS_PADDING; - // Dimensions of one page in the book - var PAGE_WIDTH = WINDOW_WIDTH; - var PAGE_HEIGHT = WINDOW_HEIGHT; - var touchStartX = 0; - var touchEndX = 0; - // Vertical spacing between the top edge of the book and the papers - var PAGE_Y = (BOOK_HEIGHT - PAGE_HEIGHT) / 2; - var pageNum = 0; - var canvas = document.getElementById("pageflip-canvas"); - if (!canvas) - return; - var context = canvas.getContext("2d"); - var mouse = { x: 0, y: 0 }; - var flips = []; - var book = document.getElementById("book"); - if (!book) - return; - // List of all the page elements in the DOM - var pages = book.getElementsByTagName("section"); - // Organize the depth of our pages and create the flip definitions - for (var i = 0, len = pages.length; i < len; i++) { - pages[i].style.zIndex = len - i + ""; - flips.push({ - // Current progress of the flip (left -1 to right +1) - progress: 1, - // The target value towards which progress is always moving - target: 1, - // The page DOM element related to this flip - page: pages[i], - // True while the page is being dragged - dragging: false, - }); - } - // Resize the canvas to match the book size - canvas.width = BOOK_WIDTH + CANVAS_PADDING * 2; - canvas.height = BOOK_HEIGHT + CANVAS_PADDING * 2; - // Offset the canvas so that it's padding is evenly spread around the book - canvas.style.top = -CANVAS_PADDING + "px"; - canvas.style.left = -CANVAS_PADDING + "px"; - // Render the page flip 60 times a second - setInterval(render, 1800 / 60); - document.addEventListener("mousemove", mouseMoveHandler, false); - document.addEventListener("mousedown", mouseDownHandler, false); - document.addEventListener("mouseup", mouseUpHandler, false); - book.addEventListener("touchmove", mouseMoveHandler, false); - book.addEventListener("touchstart", mouseDownHandler, false); - book.addEventListener("touchend", mouseUpHandler, false); - function mouseMoveHandler(event) { - if (!book) - return; - // Offset mouse position so that the top of the spine is 0,0 - // mouse.x = event.clientX - book.offsetLeft - BOOK_WIDTH / 2; - // mouse.y = event.clientY - book.offsetTop; - const touch = event.touches[0]; - const touchCurrentX = touch.screenX; - const touchCurrentY = touch.screenY; - mouse.x = touchCurrentX - book.offsetLeft - BOOK_WIDTH / 2; - mouse.y = touchCurrentY - book.offsetTop; - } - function mouseDownHandler(event) { - const touch = event.touches[0]; - // flips[page].dragging = true; - touchStartX = touch.screenX; - if (touch.screenX < window.screen.width / 2 && pageNum - 1 >= 0) { - flips[pageNum - 1].dragging = true; - } - else if (touch.screenX > window.screen.width / 2 && - pageNum + 1 < flips.length) { - flips[pageNum].dragging = true; - } - // Prevents the text selection cursor from appearing when dragging - event.preventDefault(); - } - function mouseUpHandler(event) { - const touch = event.changedTouches[0]; - touchEndX = touch.screenX; - for (var i = 0; i < flips.length; i++) { - // If this flip was being dragged we animate to its destination - if (flips[i].dragging) { - // Figure out which page we should go to next depending on the flip direction - if (mouse.x < (PAGE_WIDTH / 4) * 3 && touchEndX - touchStartX < 0) { - flips[i].target = -1; - pageNum = Math.min(pageNum + 1, flips.length); - } - else if (mouse.x > (PAGE_WIDTH / 4) * 1 && - touchEndX - touchStartX > 0) { - flips[i].target = 1; - pageNum = Math.max(pageNum - 1, 0); - } - else { - //实现当不满足以上条件时将拖拽的页面恢复到原来的位置 - if (i === pageNum) { - // Page was being dragged forward attempt - flips[i].target = 1; - } - else if (i === pageNum - 1) { - // Page was being dragged backward attempt - flips[i].target = -1; - } - } - } - flips[i].dragging = false; - } - } - function render() { - context.clearRect(0, 0, canvas.width, canvas.height); - for (var i = 0; i < flips.length; i++) { - var flip = flips[i]; - if (flip.dragging) { - flip.target = Math.max(Math.min(mouse.x / PAGE_WIDTH, 1), -1); - } - flip.progress += (flip.target - flip.progress) * 0.2; - // If the flip is being dragged or is somewhere in the middle of the book, render it - if (flip.dragging || Math.abs(flip.progress) < 0.997) { - drawFlip(flip); - } - } - } - function drawFlip(flip) { - // Strength of the fold is strongest in the middle of the book - var strength = 1 - Math.abs(flip.progress); - // Width of the folded paper - var foldWidth = PAGE_WIDTH * 0.5 * (1 - flip.progress); - // X position of the folded paper - var foldX = PAGE_WIDTH * flip.progress + foldWidth; - // How far the page should outdent vertically due to perspective - var verticalOutdent = 20 * strength; - // The maximum width of the left and right side shadows - var paperShadowWidth = PAGE_WIDTH * 0.5 * Math.max(Math.min(1 - flip.progress, 0.5), 0); - var rightShadowWidth = PAGE_WIDTH * 0.5 * Math.max(Math.min(strength, 0.5), 0); - var leftShadowWidth = PAGE_WIDTH * 0.5 * Math.max(Math.min(strength, 0.5), 0); - // Change page element width to match the x position of the fold - flip.page.style.width = Math.max(foldX, 0) + "px"; - context.save(); - context.translate(CANVAS_PADDING + BOOK_WIDTH / 2, PAGE_Y + CANVAS_PADDING); - // Draw a sharp shadow on the left side of the page - context.strokeStyle = - (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + - 0.05 * strength + - ")"; - context.lineWidth = 30 * strength; - context.beginPath(); - context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5); - context.lineTo(foldX - foldWidth, PAGE_HEIGHT + verticalOutdent * 0.5); - context.stroke(); - // Right side drop shadow - var rightShadowGradient = context.createLinearGradient(foldX, 0, foldX + rightShadowWidth, 0); - rightShadowGradient.addColorStop(0, (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + - strength * 0.2 + - ")"); - rightShadowGradient.addColorStop(0.8, (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + "0.0)"); - context.fillStyle = rightShadowGradient; - context.beginPath(); - context.moveTo(foldX, 0); - context.lineTo(foldX + rightShadowWidth, 0); - context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT); - context.lineTo(foldX, PAGE_HEIGHT); - context.fill(); - // Left side drop shadow - var leftShadowGradient = context.createLinearGradient(foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0); - leftShadowGradient.addColorStop(0, (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + "0.0)"); - leftShadowGradient.addColorStop(1, (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + - +strength * 0.15 + - ")"); - context.fillStyle = leftShadowGradient; - context.beginPath(); - context.moveTo(foldX - foldWidth - leftShadowWidth, 0); - context.lineTo(foldX - foldWidth, 0); - context.lineTo(foldX - foldWidth, PAGE_HEIGHT); - context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT); - context.fill(); - // Gradient applied to the folded paper (highlights & shadows) - var foldGradient = context.createLinearGradient(foldX - paperShadowWidth, 0, foldX, 0); - if (backgroundColor) { - foldGradient.addColorStop(0.35, backgroundColor); - foldGradient.addColorStop(0.73, backgroundColor); - foldGradient.addColorStop(0.9, backgroundColor); - foldGradient.addColorStop(1.0, backgroundColor); - } - else if (isDarkMode === "no") { - foldGradient.addColorStop(0.35, "#fafafa"); - foldGradient.addColorStop(0.73, "#eeeeee"); - foldGradient.addColorStop(0.9, "#fafafa"); - foldGradient.addColorStop(1.0, "#e2e2e2"); - } - else { - foldGradient.addColorStop(0.35, "#333"); - foldGradient.addColorStop(0.73, "#444"); - foldGradient.addColorStop(0.9, "#333"); - foldGradient.addColorStop(1.0, "#444"); - } - context.fillStyle = foldGradient; - context.strokeStyle = - (isDarkMode === "no" ? "rgba(0,0,0," : "rgba(255,255,255,") + "0.06)"; - context.lineWidth = 0.5; - // Draw the folded piece of paper - context.beginPath(); - context.moveTo(foldX, 0); - context.lineTo(foldX, PAGE_HEIGHT); - context.quadraticCurveTo(foldX, PAGE_HEIGHT + verticalOutdent * 2, foldX - foldWidth, PAGE_HEIGHT + verticalOutdent); - context.lineTo(foldX - foldWidth, -verticalOutdent); - context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0); - context.fill(); - context.stroke(); - context.restore(); - } - return { - flipToNextPage: () => { - if (pageNum + 1 < flips.length) { - flips[pageNum].target = -1; - pageNum = Math.min(pageNum + 1, flips.length); - } - }, - flipToPrevPage: () => { - if (pageNum - 1 >= 0) { - flips[pageNum - 1].target = 1; - pageNum = Math.max(pageNum - 1, 0); - } - }, - mouseDownHandler, - mouseUpHandler, - mouseMoveHandler, - }; -}; - -const getPdfScale = (element, readerMode, chapterDocList, chapterDocIndex, doc) => __awaiter(void 0, void 0, void 0, function* () { - let { width, height } = yield chapterDocList[chapterDocIndex].text.getDimension(); - let viewWidth = doc.body.clientWidth; - let viewHeight = element.clientHeight; - let scale = Math.min(viewWidth / width, viewHeight / height); - if (readerMode === "scroll") { - scale = viewWidth / width; - } - return scale; -}); -const handlePDFLayout = (element, readerMode, doc) => { - if (readerMode === "scroll") - return; - let scale = readerMode === "double" ? 2 : 1; - let section = Math.floor(doc.body.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - doc.body.setAttribute("style", element.getAttribute("style") + - `height: 100%;overflow-y: hidden;overflow-X: hidden;padding-left: 0px;padding-right: 0px;margin: 0px;box-sizing: border-box;touch-action: manipulation; overscroll-behavior: none;max-width: inherit;column-fill: auto;column-gap: ${gap}px; column-width: ${(doc.body.clientWidth - gap) / scale}px;`); -}; -const createPDFContainer = (element, chapterDocList, viewport, readerMode) => { - for (let index = 0; index < chapterDocList.length; index++) { - // Create container with aspect ratio - const iframeContainer = document.createElement("div"); - iframeContainer.style.position = "relative"; - iframeContainer.style.width = "100%"; - iframeContainer.id = "pdf-container-" + index; - iframeContainer.className = "pdf-container"; - if (readerMode === "single") { - iframeContainer.style.paddingTop = element.clientHeight + "px"; - } - else { - // Set aspect ratio based on PDF page dimensions - const aspectRatio = (viewport === null || viewport === void 0 ? void 0 : viewport.width) / (viewport === null || viewport === void 0 ? void 0 : viewport.height) || 0.75; // Default to 3:4 if viewport unknown - iframeContainer.style.paddingTop = `${(1 / aspectRatio) * 100}%`; - } - if (readerMode === "double") { - //break-inside: avoid; - iframeContainer.style.breakInside = "avoid"; - } - element.appendChild(iframeContainer); - } -}; -const createPDFIframe = (chapterDocIndex, doc) => { - var _a; - const iframeContainer = doc.getElementById("pdf-container-" + chapterDocIndex); - if (!iframeContainer) - return; - // Create iframe with absolute positioning - let iframe = document.createElement("iframe"); - iframe.style.position = "absolute"; - iframe.style.top = "0"; - iframe.style.left = "0"; - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.border = "0"; - iframe.style.margin = "0"; - iframe.style.padding = "0"; - iframe.style.fontSize = "100%"; - iframe.style.font = "inherit"; - iframe.scrolling = "no"; - iframe.tabIndex = 0; - iframe.id = "pdf-iframe-" + chapterDocIndex; - // Add style element - let style = document.createElement("style"); - style.id = "default-style"; - style.textContent = - "p,empty-line{display: inherit;margin-block-start: inherit;margin-block-end: inherit;margin-inline-start: inherit;margin-inline-end: inherit;}body{margin: 0px}"; - // Append iframe to container, then container to parent - iframeContainer.appendChild(iframe); - // Add style to iframe after it's in the DOM - (_a = iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.head.appendChild(style); - return iframe; -}; -const handleScrollPDFPosition = (chapterDocIndex, readerMode, doc) => __awaiter(void 0, void 0, void 0, function* () { - let targetNode = doc.getElementById("pdf-container-" + chapterDocIndex); - if (!targetNode) - return; - if (readerMode !== "scroll") { - let left = targetNode - ? convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)) - : 0; - doc.body.scrollTo(left, 0); - } - else { - targetNode.scrollIntoView(); - } - targetNode.scrollIntoView(); -}); -const isPDFScrolledIntoView = (element, el, readerMode, doc) => { - var isVisible = false; - var rect = el.getBoundingClientRect(); - if (readerMode !== "scroll") { - let elemLeft = rect.left; - isVisible = elemLeft > -10 && elemLeft <= doc.body.clientWidth; - } - else { - let elemTop = rect.top; - let elemBottom = rect.bottom; - isVisible = - (elemTop - 10 >= element.scrollTop && - elemTop + 10 <= element.scrollTop + element.clientHeight) || - (elemBottom - 10 >= element.scrollTop && - elemBottom + 10 <= element.scrollTop + element.clientHeight) || - (elemTop + 10 <= element.scrollTop && - elemBottom - 10 >= element.scrollTop + element.clientHeight); - } - return isVisible; -}; -const getPDFVisibleText = (chapterDocIndex, chapterDocList, readerMode) => __awaiter(void 0, void 0, void 0, function* () { - let textContent = yield chapterDocList[chapterDocIndex].text.getTextContent(); - let textList = textContent.items.map((item) => { - return item.str; - }); - if (readerMode === "double") { - let nextTextContent = yield chapterDocList[chapterDocIndex + 1].text.getTextContent(); - let nextTextList = nextTextContent.items.map((item) => { - return item.str; - }); - textList = textList.concat(nextTextList); - } - return textList; -}); -const handleHighlightPDFNode = (text, style, doc) => { - // First remove any existing highlights - const existingHighlights = doc.querySelectorAll(`span[data-highlight="true"]`); - existingHighlights.forEach((highlight) => { - // Remove only background style from the element - const style = highlight.getAttribute("style") || ""; - // Remove background or background-color properties using regex - const newStyle = style - .replace(/background(?:-color)?\s*:[^;]+;?/gi, "") - .trim(); - if (newStyle) { - highlight.setAttribute("style", newStyle); - } - else { - highlight.removeAttribute("style"); - } - highlight.removeAttribute("data-highlight"); - }); - if (!text.trim()) - return; - let nodeList = doc.querySelectorAll("p,span"); - let nodes = Array.from(nodeList).filter((s, index) => { - return ((s.textContent || "").trim() && - s.textContent === text); - }); - if (nodes.length > 0) { - nodes[0].setAttribute("style", (nodes[0].getAttribute("style") || "") + style); - nodes[0].setAttribute("data-highlight", "true"); - } -}; -const getPDFSearchResult = (keyword, chapterDocList) => __awaiter(void 0, void 0, void 0, function* () { - let searchResult = []; - for (let i = 0; i < chapterDocList.length; i++) { - let textContent = yield chapterDocList[i].text.getTextContent(); - textContent.items.forEach((item, itemIndex) => { - if (item.str.indexOf(keyword) > -1) { - searchResult.push({ - excerpt: item.str, - cfi: JSON.stringify({ - text: item.str + "#" + i + "#" + itemIndex, - chapterTitle: chapterDocList[i].label, - chapterDocIndex: i, - chapterHref: chapterDocList[i].href, - count: "search", - percentage: i / chapterDocList.length, - keyword: keyword, - }), - }); - } - }); - } - return _.uniq(searchResult, "excerpt"); -}); -const handleIOSScrollPage = (element, animation, delta, doc, flipToNextPage, flipToPrevPage, isMobile, chapterDocIndex, readerMode) => __awaiter(void 0, void 0, void 0, function* () { - let section = Math.floor(doc.body.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - const width = doc.body.clientWidth; - if (animation === "mimical" && isMobile !== "yes") { - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "block"; - if (delta > 0) { - flipToPrevPage(); - } - else if (delta < 0) { - flipToNextPage(); - } - setTimeout(() => { - if (!bookDiv) - return {}; - bookDiv.style.display = "none"; - }, 1000); - } - } - if (delta > 0) { - // previous page - if (readerMode === "single") { - let subContainer = doc.querySelector("#pdf-container-" + (chapterDocIndex - 1)); - if (subContainer) { - subContainer.scrollIntoView(); - } - } - else { - doc.body.scrollBy(-(width + gap) / 2, 0); - } - } - else if (delta < 0) { - // next page - if (readerMode === "single") { - let subContainer = doc.querySelector("#pdf-container-" + (chapterDocIndex + 1)); - if (subContainer) { - subContainer.scrollIntoView(); - } - } - else { - doc.body.scrollBy((width + gap) / 2, 0); - } - } -}); -const convertPageToImage = (page) => __awaiter(void 0, void 0, void 0, function* () { - const desiredWidth = 800; - const viewport = page.getViewport({ scale: 1 }); - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - canvas.width = desiredWidth; - canvas.height = (desiredWidth / viewport.width) * viewport.height; - const renderContext = { - canvasContext: context, - viewport: page.getViewport({ scale: desiredWidth / viewport.width }), - }; - yield page.render(renderContext).promise; - const imageURL = canvas.toDataURL("image/jpeg", 0.8); - const size = calculateSize(imageURL); - return { imageURL, size }; -}); -function calculateSize(imageURL) { - const base64Length = imageURL.length - "data:image/jpeg;base64,".length; - const sizeInBytes = Math.ceil(base64Length * 0.75); - return sizeInBytes; -} -const isElectron$1 = () => { - // Renderer process - if (typeof window !== "undefined" && - typeof window.process === "object" && - window.process.type === "renderer") { - return true; - } - // Main process - if (typeof process !== "undefined" && - typeof process.versions === "object" && - !!process.versions.electron) { - return true; - } - // Detect the user agent when the `nodeIntegration` option is set to true - if (typeof navigator === "object" && - typeof navigator.userAgent === "string" && - navigator.userAgent.indexOf("Electron") >= 0) { - return true; - } - return false; -}; -const showOCRProgress = (progress) => { - let bar = document.getElementById("ocr-progress-bar"); - if (!bar) { - bar = document.createElement("progress"); - bar.id = "ocr-progress-bar"; - bar.max = 1; - bar.value = 0; - bar.style.position = "fixed"; - bar.style.top = "10px"; - bar.style.left = "50%"; - bar.style.transform = "translateX(-50%)"; - bar.style.width = "300px"; - bar.style.zIndex = "9999"; - document.body.appendChild(bar); - } - bar.value = progress; - if (progress >= 1) { - setTimeout(() => { - bar.remove(); - }, 1000); - } -}; - -function blobUrlToBase64(blobUrl) { - return __awaiter(this, void 0, void 0, function* () { - try { - // 1. 获取Blob数据 - const response = yield fetch(blobUrl); - const blob = yield response.blob(); - // 2. 将Blob转换为Base64 - const base64 = yield new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); // 成功时返回Base64字符串 - reader.onerror = reject; // 失败时拒绝Promise - reader.readAsDataURL(blob); // 开始读取 - }); - return base64; // 返回形如 "data:image/png;base64,..." 的字符串 - } - catch (error) { - console.error("转换失败:", error); - throw error; // 抛出错误 - } - }); -} -function getScreenLeftOffset() { - if (window.visualViewport) { - return window.visualViewport.offsetLeft; - } - else { - // 回退到滚动偏移量,注意可能不准确 - return window.pageXOffset || document.documentElement.scrollLeft || 0; - } -} -function getScreenTopOffset() { - if (window.visualViewport) { - return window.visualViewport.offsetTop; - } - else { - // 回退到滚动偏移量,注意可能不准确 - return window.pageYOffset || document.documentElement.scrollTop || 0; - } -} -const preventLinkNavigation = (event, doc, render) => __awaiter(void 0, void 0, void 0, function* () { - const target = event.target; - if (!target) - return; - const linkElement = findLinkElement(target); - if (linkElement) { - event.preventDefault(); - event.stopPropagation(); - // Get href from the link - let href = linkElement.getAttribute("href"); - if (href && href.startsWith("kindle:")) { - let chapterInfo = render.resolveChapter(href); - if (chapterInfo) { - yield render.goToChapter(chapterInfo.index, chapterInfo.href, chapterInfo.label); - return true; - } - let result = yield render.resolveHref(href); - href = "#" + result.id; - } - let footnote = ""; - if (href && href.indexOf("#") > -1) { - let id = href.split("#").reverse()[0]; - let node = doc.body.querySelector("#" + id); - if (!node) { - if (href.indexOf("filepos") > -1) { - let chapterInfo = render.resolveChapter(href); - yield render.goToChapter(chapterInfo.index, chapterInfo.href, chapterInfo.label); - return true; - } - if (href.indexOf("#") !== 0) { - while (href.startsWith(".")) { - href = href.substring(1); - } - let chapterInfo = render.resolveChapter(href); - if (chapterInfo) { - yield render.goToChapter(chapterInfo.index, chapterInfo.href, chapterInfo.label); - } - } - node = doc.body.querySelector("#" + CSS.escape(id)); - if (!node) { - return false; - } - yield render.goToNode(doc.body.querySelector("#" + CSS.escape(id))); - } - if ((node.textContent.trim() === event.target.textContent.trim() || - !node.textContent.trim()) && - node.parentElement) { - if (node.parentElement.tagName !== "BODY") { - node = node.parentElement; - } - else { - return false; - } - } - //将html代码中的img标签由blob转换为base64 - footnote = node.textContent; - } - else if (href) { - let chapterInfo = render.resolveChapter(href); - if (chapterInfo) { - yield render.goToChapter(chapterInfo.index, chapterInfo.href, chapterInfo.label); - } - } - // Send message to React Native with link details - window.ReactNativeWebView.postMessage(JSON.stringify({ - event: "link-clicked", - href: href, - footnote: footnote, - })); - return false; - } -}); -function findLinkElement(element) { - // Check if the element itself is a link - if (element.tagName === "A") { - return element; - } - // Traverse up the DOM tree to check for parent links - // This handles cases where the click target is a child element inside a link - let currentElement = element; - while (currentElement && currentElement.tagName !== "BODY") { - if (currentElement.tagName === "A") { - return currentElement; - } - currentElement = currentElement.parentElement; - } - return null; -} -function getTouchAction(col, row, touchControlRule) { - //根据col和row获取对应的区域编号,从左到右,从上到下,1-9 - const areaIndex = row * 3 + col + 1; // 1-9 - //如果区域编号在touchControlRule中存在,则返回对应的action - if (touchControlRule.layout["A"].area.includes(areaIndex)) { - return touchControlRule["touchControlA"]; - } - else if (touchControlRule.layout["B"].area.includes(areaIndex)) { - return touchControlRule["touchControlB"]; - } - else if (touchControlRule.layout["C"].area.includes(areaIndex)) { - return touchControlRule["touchControlC"]; - } - return "right"; -} -const addAndroidTouchEvent = (doc, iframe, element, readerMode, animation, format, touchControlRule, render) => { - var _a; - let iWin = iframe.contentWindow || ((_a = iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.defaultView); - let outerDoc = render.getDocument(); - let touchStartTime = 0; - let touchStartX = 0; - let touchStartY = 0; - let lastTouchEnd = 0; - const swipeThreshold = 30; // Minimum distance in pixels to be considered a swipe - const timeThreshold = 500; // Maximum time in milliseconds to be considered a tap - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let pageWidth = element.clientWidth + gap; - let pageChangeDebounceTimer = null; - let onTouchEnd = function (event) { - window.isSwiping = false; - window.isTouchNavigation = true; - if (pageChangeDebounceTimer) { - clearTimeout(pageChangeDebounceTimer); - } - pageChangeDebounceTimer = setTimeout(() => { - window.isTouchNavigation = false; - pageChangeDebounceTimer = null; - }, 4000); - let now = new Date().getTime(); - if (now - lastTouchEnd <= 300) { - event.preventDefault(); - return; - } - lastTouchEnd = now; - const touch = event.changedTouches[0]; - const touchEndTime = Date.now(); - let touchEndX = touch.screenX; - let touchEndY = touch.screenY; - const timeDiff = touchEndTime - touchStartTime; - const distX = touchEndX - touchStartX; - const distY = touchEndY - touchStartY; - if (isDragging && animation === "mimical" && readerMode !== "scroll") { - isDragging = false; - render.mouseUpHandler(event); - if (touch.screenX < (window.screen.width / 4) * 3 && - touchEndX - touchStartX < 0) { - render.next(); - isDragging = false; - } - else if (touch.screenX > (window.screen.width / 4) * 1 && - touchEndX - touchStartX > 0) { - render.prev(); - isDragging = false; - } - setTimeout(() => { - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "none"; - } - }, 400); - return; - } - // Replace the scrollTo implementation with this optimized version - if (isDragging && animation === "sliding" && readerMode !== "scroll") { - let tempDoc = format === "PDF" ? outerDoc : doc; - // Clean up any existing animation - if (window.scrollAnimationId) { - cancelAnimationFrame(window.scrollAnimationId); - } - if (Math.abs(tempDoc.body.scrollWidth - - tempDoc.body.scrollLeft - - element.clientWidth) < 10) { - if (selectionTimeout) { - clearTimeout(selectionTimeout); - } - selectionTimeout = setTimeout(() => { - render.next(); - isDragging = false; - }, 300); // Debounce selection events - return; - } - if (tempDoc.body.scrollLeft === 0) { - if (selectionTimeout) { - clearTimeout(selectionTimeout); - } - selectionTimeout = setTimeout(() => { - render.prev(); - isDragging = false; - }, 300); // Debounce selection events - return; - } - tempDoc.body.style.transform = ""; - let scrollLeft = tempDoc.body.scrollLeft; - // Improved snapping logic - let snapX; - const currentPage = Math.round(scrollLeft / pageWidth); - const dragPercentage = Math.abs(distX) / window.screen.width; - const dragThreshold = 0.1; // Only 10% drag needed to change page - if (distX > 0 && dragPercentage > dragThreshold) { - // Dragged right (go to previous page) - snapX = (currentPage - 1) * pageWidth; - } - else if (distX < 0 && dragPercentage > dragThreshold) { - // Dragged left (go to next page) - snapX = (currentPage + 1) * pageWidth; - } - else { - // Stay on current page - snapX = currentPage * pageWidth; - } - // Ensure we don't go out of bounds - snapX = Math.max(0, Math.min(snapX, tempDoc.body.scrollWidth - pageWidth)); - if (tempDoc.body.scrollWidth - snapX < pageWidth + gap) { - snapX = tempDoc.body.scrollWidth; - } - // Use custom smooth scrolling with requestAnimationFrame instead of browser's scrollTo - const startTime = performance.now(); - const startLeft = tempDoc.body.scrollLeft; - const distance = snapX - startLeft; - const duration = 300; // milliseconds - // Apply hardware acceleration before animation starts - tempDoc.body.style.willChange = "scroll-position"; - // Custom easing function for natural movement - const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); - function animateScroll(currentTime) { - const elapsedTime = currentTime - startTime; - if (elapsedTime >= duration) { - // Animation complete - set final position - tempDoc.body.scrollLeft = snapX; - // Clean up acceleration hints - tempDoc.body.style.willChange = "auto"; - render.record(); - isDragging = false; - return; - } - // Calculate new position using easing - const progress = easeOutCubic(elapsedTime / duration); - const newLeft = startLeft + distance * progress; - // Update scroll position - tempDoc.body.scrollLeft = newLeft; - // Continue animation - window.scrollAnimationId = requestAnimationFrame(animateScroll); - } - // Start animation - window.scrollAnimationId = requestAnimationFrame(animateScroll); - return; - } - var selectedText = iWin.getSelection().toString(); - var isSwiping = Math.abs(distX) >= swipeThreshold || Math.abs(distY) >= swipeThreshold; - if (selectedText && - (format !== "PDF" || (format === "PDF" && !isSwiping))) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - event: "select-text-after-touch", - selectedText: selectedText, - })); - return; - } - if (timeDiff > timeThreshold) { - const target = event.target; - if (!target) - return; - if (target.tagName === "IMG" || target.tagName === "image") { - const imgSrc = target.src || target.getAttribute("xlink:href"); - //blob to base64 - if (imgSrc.startsWith("blob:")) { - blobUrlToBase64(imgSrc).then((base64) => { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "view-image", imgSrc: base64 })); - }); - } - return; - } - } - if (timeDiff < timeThreshold && - Math.abs(distX) < swipeThreshold && - Math.abs(distY) < swipeThreshold) { - var width = window.screen.width; - var height = window.screen.height; - var cellWidth = width / 3; - var cellHeight = height / 3; - var col = Math.floor(touchEndX / cellWidth); - var row = Math.floor(touchEndY / cellHeight); - var result = getTouchAction(col, row, touchControlRule); - window.ReactNativeWebView.postMessage(JSON.stringify({ event: result })); - } - else if (Math.abs(distX) >= swipeThreshold || - Math.abs(distY) >= swipeThreshold) { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "swipe" })); - if (readerMode === "scroll" && - Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight) < 10) { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "scroll-bottom" })); - } - if (readerMode === "scroll" && element.scrollTop === 0) { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "scroll-top" })); - } - } - }; - let onTouchStart = function (event) { - touchStartTime = Date.now(); - const target = event.target; - if (!target) - return; - const linkElement = findLinkElement(target); - if (linkElement) { - return; - } - if (event.touches.length > 1) { - event.preventDefault(); - } - const touch = event.touches[0]; - touchStartX = touch.screenX; - touchStartY = touch.screenY; - }; - let isDragging = false; - let lastTouchX = 0; - let onTouchMove = function (event) { - // Skip handling if not dragging yet and still determining direction - if (!isDragging && Math.abs(event.touches[0].screenX - touchStartX) <= 10) { - return; - } - // Prevent default to stop browser scroll behavior - event.preventDefault(); - if (window.visualViewport.scale > 1 && format === "PDF") { - event.preventDefault(); - return; - } - const touch = event.touches[0]; - const touchCurrentX = touch.screenX; - const touchCurrentY = touch.screenY; - // Calculate distance moved - const distX = touchCurrentX - touchStartX; - const distY = touchCurrentY - touchStartY; - if (Math.abs(distX) > 10 || Math.abs(distY) > 10) { - window.isSwiping = true; - } - // Only start dragging if horizontal movement is greater than vertical - if (!isDragging && - Math.abs(distX) > Math.abs(distY) && - Math.abs(distX) > 10) { - isDragging = true; - lastTouchX = touchCurrentX; - // Apply hardware acceleration to the body - doc.body.style.transform = "translateZ(0)"; - if (animation === "mimical" && readerMode !== "scroll") { - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "block"; - render.mouseDownHandler(event); - } - } - return; - } - if (isDragging && animation === "mimical" && readerMode !== "scroll") { - render.mouseMoveHandler(event); - } - // If we're in dragging mode, apply direct transform for better performance - if (isDragging && animation === "sliding" && readerMode !== "scroll") { - let tempDoc = format === "PDF" ? outerDoc : doc; - // Calculate the delta since last move event - const deltaX = touchCurrentX - lastTouchX; - const currentScrollLeft = tempDoc.body.scrollLeft; - tempDoc.body.scrollLeft = currentScrollLeft - deltaX; - // Update last position - lastTouchX = touchCurrentX; - // Request animation frame for smoother updates (optional) - requestAnimationFrame(() => { - // Additional visual feedback can be added here - }); - } - }; - doc.addEventListener("touchend", onTouchEnd, false); - doc.addEventListener("touchstart", onTouchStart, false); - doc.addEventListener("touchmove", onTouchMove, false); - // Add this with the other event listeners - doc.addEventListener("click", (event) => { - preventLinkNavigation(event, doc, render); - }, true); // Use capturing phase - let selectionTimeout = null; - let startSelectionTime = 0; - let selectionCount = 0; - let triggerSelectionMenu = (event) => __awaiter(void 0, void 0, void 0, function* () { - var _b, _c; - const selectedText = iWin.getSelection().toString().trim(); - if (selectedText) { - var range = iWin.getSelection().getRangeAt(0); - let pageSize = render.getPageSize(); - var rect = range.getBoundingClientRect(); - if (format === "PDF") { - let clientRects = range.getClientRects(); - if (clientRects.length > 0) { - //combine all the rects - clientRects = Array.from(clientRects).filter((item) => { - return (Math.abs(item.height - pageSize.sectionHeight) > 10 && - Math.abs(item.width - pageSize.sectionWidth) > 10 && - item.height > 0 && - item.width > 0); - }); - let minTop = Infinity; - let minLeft = Infinity; - let maxBottom = -Infinity; - let maxRight = -Infinity; - for (let i = 0; i < clientRects.length; i++) { - const rect = clientRects[i]; - minTop = Math.min(minTop, rect.top); - minLeft = Math.min(minLeft, rect.left); - maxBottom = Math.max(maxBottom, rect.bottom); - maxRight = Math.max(maxRight, rect.right); - } - // Create the combined rectangle object - const combinedRect = { - top: minTop, - left: minLeft, - bottom: maxBottom, - right: maxRight, - width: maxRight - minLeft, - height: maxBottom - minTop, - }; - rect = combinedRect; - } - } - var position = { - top: rect.top - element.scrollTop, - left: rect.left, - width: rect.width, - height: rect.height, - screenWidth: window.innerWidth, - screenHeight: window.innerHeight, - sectionHeight: pageSize.sectionHeight, - sectionWidth: pageSize.sectionWidth, - gap: pageSize.gap, - scale: window.visualViewport.scale, - offsetLeft: offsetLeft, - offsetTop: offsetTop, - }; - rangy.init(); - let charRange = null; - if (format === "PDF") { - try { - let target = event.target; - let targetIframe = (_c = (_b = target.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.frameElement; - let id = (targetIframe === null || targetIframe === void 0 ? void 0 : targetIframe.getAttribute("id")) || ""; - let chapterDocIndex = id ? parseInt(id.split("-").reverse()[0]) : 0; - charRange = yield render.getHightlightCoords(chapterDocIndex); - position.chapterDocIndex = chapterDocIndex; - let subContainer = targetIframe.parentElement; - if (subContainer) { - position.top = - position.top + parseFloat(getComputedStyle(subContainer).top); - } - } - catch (error) { - console.error("Error getting highlight coords:", error); - } - } - else { - charRange = yield render.getHightlightCoords(); - } - window.ReactNativeWebView.postMessage(JSON.stringify({ - event: "select-text", - selectedText: selectedText, - position: position, - range: charRange, - })); - } - }); - doc.body.oncontextmenu = function (event) { - const target = event.target; - if (!target) - return; - if (target.tagName === "IMG" || target.tagName === "image") { - const imgSrc = target.src || target.getAttribute("xlink:href"); - //blob to base64 - if (imgSrc.startsWith("blob:")) { - blobUrlToBase64(imgSrc).then((base64) => { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "view-image", imgSrc: base64 })); - }); - } - return; - } - if (Date.now() - startSelectionTime < 100) { - setTimeout(() => { - if (selectionCount === 1) { - triggerSelectionMenu(event); - } - }, 600); - } - else { - triggerSelectionMenu(event); - } - event.preventDefault(); - event.stopPropagation(); - return false; - }; - let scrollLeft = 0; - let offsetLeft = 0; - let offsetTop = 0; - doc.addEventListener("selectstart", (event) => { - selectionCount = 0; - startSelectionTime = Date.now(); - offsetLeft = getScreenLeftOffset(); - offsetTop = getScreenTopOffset(); - if (readerMode === "scroll") - return; - scrollLeft = doc.body.scrollLeft; - //prevent doc.body from scrolling - }, false); - let lastSelectionChangeTime = 0; - const SELECTION_THROTTLE_DELAY = 3000; // 3秒 - doc.addEventListener("selectionchange", (event) => { - //检查选择文字是否为空 - const selectedText = iWin.getSelection().toString().trim(); - if (!selectedText) { - return; - } - if (scrollLeft > 0) { - doc.body.scrollLeft = scrollLeft; - } - selectionCount++; - const now = Date.now(); - // 检查是否超过3秒间隔 - if (now - lastSelectionChangeTime >= SELECTION_THROTTLE_DELAY) { - // 更新最后触发时间 - lastSelectionChangeTime = now; - // 执行原有逻辑 - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "selection-change" })); - } - }, false); -}; -const addAppleTouchEvent = (doc, iframe, element, readerMode, animation, format, touchControlRules, render) => { - var _a; - let iWin = iframe.contentWindow || ((_a = iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.defaultView); - let outerDoc = render.getDocument(); - let touchStartTime = 0; - let touchStartX = 0; - let touchStartY = 0; - let lastTouchEnd = 0; - let swipeThreshold = 30; // Minimum distance in pixels to be considered a swipe - const timeThreshold = 500; // Maximum time in milliseconds to be considered a tap - let section = Math.floor(element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let pageChangeDebounceTimer = null; - let onTouchEnd = function (event) { - var _a, _b; - return __awaiter(this, void 0, void 0, function* () { - window.isSwiping = false; - window.isTouchNavigation = true; - if (pageChangeDebounceTimer) { - clearTimeout(pageChangeDebounceTimer); - } - pageChangeDebounceTimer = setTimeout(() => { - window.isTouchNavigation = false; - pageChangeDebounceTimer = null; - }, 4000); - let now = new Date().getTime(); - if (now - lastTouchEnd <= 300) { - event.preventDefault(); - return; - } - lastTouchEnd = now; - const touch = event.changedTouches[0]; - const touchEndTime = Date.now(); - const touchEndX = touch.screenX; - const touchEndY = touch.screenY; - const timeDiff = touchEndTime - touchStartTime; - const distX = touchEndX - touchStartX; - const distY = touchEndY - touchStartY; - if (isDragging && animation === "mimical" && readerMode !== "scroll") { - isDragging = false; - render.mouseUpHandler(event); - if (touchEndX < (window.screen.width / 4) * 3 && - touchEndX - touchStartX < 0) { - render.next(); - isDragging = false; - } - else if (touchEndX > (window.screen.width / 4) * 1 && - touchEndX - touchStartX > 0) { - render.prev(); - isDragging = false; - } - setTimeout(() => { - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "none"; - } - }, 400); - return; - } - // Replace the scrollTo implementation with this optimized version - if (isDragging && animation === "sliding" && readerMode !== "scroll") { - let tempDoc = format === "PDF" ? outerDoc : doc; - // Clean up any existing animation - if (window.scrollAnimationId) { - cancelAnimationFrame(window.scrollAnimationId); - } - if (Math.abs(tempDoc.body.scrollWidth - - tempDoc.body.scrollLeft - - element.clientWidth) < 10) { - if (selectionTimeout) { - clearTimeout(selectionTimeout); - } - selectionTimeout = setTimeout(() => { - render.next(); - isDragging = false; - }, 300); // Debounce selection events - return; - } - if (tempDoc.body.scrollLeft === 0) { - if (selectionTimeout) { - clearTimeout(selectionTimeout); - } - selectionTimeout = setTimeout(() => { - render.prev(); - isDragging = false; - }, 300); // Debounce selection events - return; - } - tempDoc.body.style.transform = ""; - let pageWidth = element.clientWidth + gap; - let scrollLeft = tempDoc.body.scrollLeft; - // Improved snapping logic - let snapX; - const currentPage = Math.round(scrollLeft / pageWidth); - const dragPercentage = Math.abs(distX) / window.screen.width; - const dragThreshold = 0.1; // Only 10% drag needed to change page - if (distX > 0 && dragPercentage > dragThreshold) { - // Dragged right (go to previous page) - snapX = (currentPage - 1) * pageWidth; - } - else if (distX < 0 && dragPercentage > dragThreshold) { - // Dragged left (go to next page) - snapX = (currentPage + 1) * pageWidth; - } - else { - // Stay on current page - snapX = currentPage * pageWidth; - } - // Ensure we don't go out of bounds - snapX = Math.max(0, Math.min(snapX, tempDoc.body.scrollWidth - pageWidth)); - if (tempDoc.body.scrollWidth - snapX < pageWidth + gap) { - snapX = tempDoc.body.scrollWidth; - } - // Use custom smooth scrolling with requestAnimationFrame instead of browser's scrollTo - const startTime = performance.now(); - const startLeft = tempDoc.body.scrollLeft; - const distance = snapX - startLeft; - const duration = 300; // milliseconds - // Apply hardware acceleration before animation starts - tempDoc.body.style.willChange = "scroll-position"; - // Custom easing function for natural movement - const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); - function animateScroll(currentTime) { - const elapsedTime = currentTime - startTime; - if (elapsedTime >= duration) { - // Animation complete - set final position - tempDoc.body.scrollLeft = snapX; - // Clean up acceleration hints - tempDoc.body.style.willChange = "auto"; - render.record(); - isDragging = false; - return; - } - // Calculate new position using easing - const progress = easeOutCubic(elapsedTime / duration); - const newLeft = startLeft + distance * progress; - // Update scroll position - tempDoc.body.scrollLeft = newLeft; - // Continue animation - window.scrollAnimationId = requestAnimationFrame(animateScroll); - } - // Start animation - window.scrollAnimationId = requestAnimationFrame(animateScroll); - return; - } - const selectedText = iWin.getSelection().toString().trim(); - if (selectedText) { - var range = iWin.getSelection().getRangeAt(0); - var rect = range.getBoundingClientRect(); - var pageSize = render.getPageSize(); - var position = { - top: rect.top - element.scrollTop, - left: rect.left, - width: rect.width, - height: rect.height, - screenWidth: window.innerWidth, - screenHeight: window.innerHeight, - sectionHeight: pageSize.sectionHeight, - sectionWidth: pageSize.sectionWidth, - gap: pageSize.gap, - scale: window.visualViewport.scale, - offsetLeft: getScreenLeftOffset(), - offsetTop: getScreenTopOffset(), - }; - rangy.init(); - let charRange = null; - if (format === "PDF") { - let target = event.target; - let ownerDoc = target.ownerDocument; - let targetIframe = (_a = ownerDoc === null || ownerDoc === void 0 ? void 0 : ownerDoc.defaultView) === null || _a === void 0 ? void 0 : _a.frameElement; - let id = (targetIframe === null || targetIframe === void 0 ? void 0 : targetIframe.getAttribute("id")) || ""; - let chapterDocIndex = id ? parseInt(id.split("-").reverse()[0]) : 0; - position.chapterDocIndex = chapterDocIndex; - charRange = yield render.getHightlightCoords(chapterDocIndex); - let subContainer = targetIframe.parentElement; - if (subContainer) { - position.top = - position.top + parseFloat(getComputedStyle(subContainer).top); - } - } - else { - charRange = yield render.getHightlightCoords(); - } - window.ReactNativeWebView.postMessage(JSON.stringify({ - event: "select-text", - selectedText: selectedText, - position: position, - range: charRange, - })); - return; - } - if (timeDiff > timeThreshold) { - const target = event.target; - if (!target) - return; - if (target.tagName === "IMG" || target.tagName === "image") { - const imgSrc = target.src || target.getAttribute("xlink:href"); - //blob to base64 - if (imgSrc.startsWith("blob:")) { - blobUrlToBase64(imgSrc).then((base64) => { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "view-image", imgSrc: base64 })); - }); - } - return; - } - } - if (timeDiff < timeThreshold && - Math.abs(distX) < swipeThreshold && - Math.abs(distY) < swipeThreshold) { - const width = document.documentElement.clientWidth; - const height = document.documentElement.clientHeight; - let normalizedX = Math.min(Math.max(touchEndX, 0), width); - let normalizedY = Math.min(Math.max(touchEndY, 0), height); - if (format === "PDF" && readerMode === "double") { - let target = event.target; - let ownerDoc = target.ownerDocument; - let targetIframe = (_b = ownerDoc === null || ownerDoc === void 0 ? void 0 : ownerDoc.defaultView) === null || _b === void 0 ? void 0 : _b.frameElement; - let id = (targetIframe === null || targetIframe === void 0 ? void 0 : targetIframe.getAttribute("id")) || ""; - let chapterDocIndex = id ? parseInt(id.split("-").reverse()[0]) : 0; - if (chapterDocIndex % 2 === 1) { - normalizedX = normalizedX + width / 2; - } - } - // For pagination mode: keep original 3x3 grid - const cellWidth = width / 3; - const cellHeight = height / 3; - const col = Math.min(Math.floor(normalizedX / cellWidth), 2); - const row = Math.min(Math.floor(normalizedY / cellHeight), 2); - let result = getTouchAction(col, row, touchControlRules); - window.ReactNativeWebView.postMessage(JSON.stringify({ event: result })); - } - else if (Math.abs(distX) >= swipeThreshold || - Math.abs(distY) >= swipeThreshold) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - event: "swipe", - })); - if (readerMode === "scroll" && - Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight) < 10) { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "scroll-bottom" })); - } - if (readerMode === "scroll" && element.scrollTop === 0) { - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "scroll-top" })); - } - } - }); - }; - let onTouchStart = function (event) { - const target = event.target; - if (!target) - return; - const linkElement = findLinkElement(target); - if (linkElement) { - return; - } - //// 注释掉解决无法双指缩放pdf的问题 - // if (event.touches.length > 1) { - // event.preventDefault(); - // } - const touch = event.touches[0]; - touchStartTime = Date.now(); - touchStartX = touch.screenX; - touchStartY = touch.screenY; - }; - let isDragging = false; - let lastTouchX = 0; - let onTouchMove = function (event) { - const selectedText = iWin.getSelection().toString().trim(); - // Skip handling if not dragging yet and still determining direction - if ((!isDragging && Math.abs(event.touches[0].screenX - touchStartX) <= 10) || - selectedText) { - return; - } - if (window.visualViewport.scale > 1 && format === "PDF") { - return; - } - const touch = event.touches[0]; - const touchCurrentX = touch.screenX; - const touchCurrentY = touch.screenY; - // Calculate distance moved - const distX = touchCurrentX - touchStartX; - const distY = touchCurrentY - touchStartY; - // Only start dragging if horizontal movement is greater than vertical - if (!isDragging && - Math.abs(distX) > Math.abs(distY) && - Math.abs(distX) > 10) { - isDragging = true; - lastTouchX = touchCurrentX; - if (animation === "mimical" && readerMode !== "scroll") { - window.isSwiping = true; - let bookDiv = document.getElementById("book"); - if (bookDiv) { - bookDiv.style.display = "block"; - render.mouseDownHandler(event); - } - } - return; - } - if (isDragging && animation === "mimical" && readerMode !== "scroll") { - render.mouseMoveHandler(event); - } - // If we're in dragging mode, apply direct transform for better performance - if (isDragging && animation === "sliding" && readerMode !== "scroll") { - window.isSwiping = true; - let tempDoc = format === "PDF" ? outerDoc : doc; - // Calculate the delta since last move event - const deltaX = touchCurrentX - lastTouchX; - // Use transform instead of scrollBy for smoother rendering - const currentScrollLeft = tempDoc.body.scrollLeft; - tempDoc.body.scrollLeft = currentScrollLeft - deltaX; - // Update last position - lastTouchX = touchCurrentX; - // Request animation frame for smoother updates (optional) - requestAnimationFrame(() => { - // Additional visual feedback can be added here - }); - } - }; - // Add passive: false to ensure preventDefault works - doc.addEventListener("touchend", onTouchEnd, { passive: false }); - doc.addEventListener("touchstart", onTouchStart, { passive: false }); - doc.addEventListener("touchmove", onTouchMove, { passive: false }); - // Add this with the other event listeners - doc.addEventListener("click", (event) => { - preventLinkNavigation(event, doc, render); - }, true); // Use capturing phase - let selectionTimeout = null; - doc.body.oncontextmenu = function (event) { - event.preventDefault(); - event.stopPropagation(); - return false; - }; - let lastSelectionChangeTime = 0; - const SELECTION_THROTTLE_DELAY = 3000; // 3秒 - doc.addEventListener("selectionchange", (event) => { - //检查选择文字是否为空 - const selectedText = iWin.getSelection().toString().trim(); - if (!selectedText) { - return; - } - const now = Date.now(); - // 检查是否超过3秒间隔 - if (now - lastSelectionChangeTime >= SELECTION_THROTTLE_DELAY) { - // 更新最后触发时间 - lastSelectionChangeTime = now; - // 执行原有逻辑 - window.ReactNativeWebView.postMessage(JSON.stringify({ event: "selection-change" })); - } - }, { passive: false }); -}; - -class GeneralRender extends EventEmitter { - constructor(config) { - super(); - this.tranformText = () => { - let doc = this.getDocument(); - if (!doc) - return; - if (this.convertChinese === "Simplified To Traditional") { - doc - .querySelectorAll("h1,h2,h3,h4,h5,h6,p,div,ul,dl,ol,pre,li,dt,dd,blockquote,address,kookitmarker") - .forEach((item) => { - item.innerHTML = item.innerHTML - .split("") - .map((item) => Chinese.s2t(item)) - .join(""); - }); - } - else if (this.convertChinese === "Traditional To Simplified") { - doc - .querySelectorAll("h1,h2,h3,h4,h5,h6,p,div,ul,dl,ol,pre,li,dt,dd,blockquote,address,kookitmarker") - .forEach((item) => { - item.innerHTML = item.innerHTML - .split("") - .map((item) => Chinese.t2s(item)) - .join(""); - }); - } - //确保页面完全加载完毕之后,在修改缩进 - if (this.isIndent === "yes") { - doc - .querySelectorAll("h1,h2,h3,h4,h5,h6,p,div,ul,dl,ol,pre,li,dt,dd,blockquote,address") - .forEach((item) => { - for (let node of item.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - // 将前导空格替换为零宽度字符,保留原始内容但不显示 - const text = node.nodeValue || ""; - const leadingSpaces = text.match(/^(\s+)/); - if (leadingSpaces) { - //覆盖父元素的text-indent css - item.setAttribute("style", (item.getAttribute("style") || "") + - "text-indent: 0em !important;"); - } - // 只处理第一个,退出循环 - break; - } - //如果子元素为img则也缩进设为0 - if (node.nodeType === Node.ELEMENT_NODE && - node.tagName.toLowerCase() === "img") { - item.setAttribute("style", (item.getAttribute("style") || "") + - "text-indent: 0em !important;"); - break; - } - } - }); - } - }; - this.addPageAnimation = (backgroundColor) => { - if (this.animation === "mimical") { - let progressInfo = this.getProgress(); - if (!progressInfo) - return; - const pageAnimation = addPageAnimation(progressInfo.totalPage, this.isDarkMode, backgroundColor); - if (pageAnimation) { - this.flipToNextPage = pageAnimation.flipToNextPage; - this.flipToPrevPage = pageAnimation.flipToPrevPage; - this.mouseDownHandler = pageAnimation.mouseDownHandler; - this.mouseUpHandler = pageAnimation.mouseUpHandler; - this.mouseMoveHandler = pageAnimation.mouseMoveHandler; - } - } - }; - this.readerMode = config.readerMode; - this.animation = config.animation; - this.format = config.format; - this.convertChinese = config.convertChinese; - this.isIndent = config.isIndent; - this.isDarkMode = config.isDarkMode; - this.isMobile = config.isMobile; - this.chapterList = []; - this.chapterDocList = []; - this.flattenChapters = []; - this.book = ""; - this.element = ""; - this.tempLocation = {}; - this.flipToNextPage = () => { }; - this.flipToPrevPage = () => { }; - this.mouseDownHandler = () => { }; - this.mouseUpHandler = () => { }; - this.mouseMoveHandler = (event) => { }; - this.touchEventSet = {}; - if (this.isMobile === "yes") { - console.log = function (...args) { - window.ReactNativeWebView.postMessage(args.map((arg) => String(arg)).join(", ")); - }; - console.info = function (...args) { - window.ReactNativeWebView.postMessage(args.map((arg) => String(arg)).join(", ")); - }; - console.error = function (...args) { - window.ReactNativeWebView.postMessage(args.map((arg) => String(arg)).join(", ")); - }; - } - } - getPageSize() { - let scale = this.readerMode === "double" ? 2 : 1; - let section = Math.floor(this.element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - let iframe = this.getIframe(); - if (!iframe) - return; - let iframeHeight = iframe === null || iframe === void 0 ? void 0 : iframe.getBoundingClientRect().height; - return { - width: this.element.clientWidth, - height: this.element.clientHeight, - left: this.element.offsetLeft, - top: this.element.offsetTop, - scrollTop: this.element.scrollTop, - sectionWidth: (this.element.clientWidth - gap) / scale, - sectionHeight: iframeHeight, - gap: gap, - }; - } - scrollToText(text) { - let doc = this.getDocument(); - if (!doc) - return; - let nodeList = getBlockElement(doc.body).filter((item) => !isParentBlock(item)); - let audioNodes = nodeList.filter((s) => (s.textContent || "").indexOf(text) > -1); - if (audioNodes.length > 0) { - let targetNode = audioNodes[0]; - let left = targetNode - ? convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)) - : 0; - let top = targetNode - ? convertStyleNum(targetNode.offsetTop) - - convertStyleNum(targetNode.marginTop || - parseFloat(getComputedStyle(targetNode).marginTop)) - : 0; - if (this.readerMode !== "scroll") { - doc.body.scrollTo(left, 0); - } - else { - this.element.scrollTo(0, top); - } - } - } - goToPage(targetPage) { - return __awaiter(this, void 0, void 0, function* () { - if (this.readerMode === "scroll") { - if (targetPage < 0) { - targetPage = 1; - } - let top = (targetPage - 1) * (this.element.clientHeight - 50); - this.element.scrollTo(0, top); - } - else { - let doc = this.getDocument(); - if (!doc) - return; - let section = Math.floor(this.element.clientWidth / 12); - let gap = section % 2 === 0 ? section : section - 1; - const width = this.element.clientWidth; - const scrollDistance = width + gap; - if (this.readerMode === "double") { - targetPage = - (targetPage % 2 === 0 ? targetPage - 2 : targetPage - 1) / 2; - } - else { - targetPage = targetPage - 1; - } - if (targetPage < 0) { - targetPage = 0; - } - const targetScrollLeft = targetPage * scrollDistance; - doc.body.scrollTo({ - top: 0, - left: targetScrollLeft, - behavior: this.animation === "sliding" && this.isMobile !== "yes" - ? "smooth" - : "auto", - }); - } - yield this.record(); - }); - } - resolveChapter(href) { - let path = href; - let chapterIndex = -1; - for (let index = 0; index < this.flattenChapters.length; index++) { - if (this.flattenChapters[index].href.includes(path)) { - chapterIndex = index; - break; - } - } - if (chapterIndex > -1) { - return this.flattenChapters[chapterIndex]; - } - else { - let pathWithoutHash = href.split("#")[0]; - for (let index = 0; index < this.flattenChapters.length; index++) { - if (this.flattenChapters[index].href.includes(pathWithoutHash.substring(1))) { - chapterIndex = index; - break; - } - } - if (chapterIndex > -1) { - return this.flattenChapters[chapterIndex]; - } - else { - for (let index = 0; index < this.chapterDocList.length; index++) { - if (this.chapterDocList[index].text && - this.chapterDocList[index].text.id && - (this.chapterDocList[index].text.id + "").includes(path)) { - chapterIndex = index; - break; - } - } - if (chapterIndex > -1) { - return { label: "", href: "", index: chapterIndex }; - } - else { - return null; - } - } - } - } - flatChapter(chapters) { - let newChapter = []; - for (let i = 0; i < chapters.length; i++) { - if (chapters[i].subitems && chapters[i].subitems.length > 0) { - newChapter.push(chapters[i]); - newChapter = newChapter.concat(this.flatChapter(chapters[i].subitems)); - } - else { - newChapter.push(chapters[i]); - } - } - this.flattenChapters = newChapter; - return newChapter; - } - getChapter() { - return this.chapterList; - } - getChapterDoc() { - return this.chapterDocList; - } - goToPercentage(percentage) { - return __awaiter(this, void 0, void 0, function* () { - if (this.flattenChapters.length > 0) { - let chapterIndex = percentage === 1 - ? this.flattenChapters.length - 1 - : Math.floor(this.flattenChapters.length * percentage); - yield this.goToChapter(this.flattenChapters[chapterIndex].index.toString(), this.flattenChapters[chapterIndex].href, this.flattenChapters[chapterIndex].label); - } - }); - } - goToChapterIndex(targetChapterIndex) { - return __awaiter(this, void 0, void 0, function* () { - if (this.flattenChapters.length > 0) { - yield this.goToChapter(this.flattenChapters[targetChapterIndex].index, this.flattenChapters[targetChapterIndex].href, this.flattenChapters[targetChapterIndex].label); - } - }); - } - goToChapterDocIndex(chapterDocIndex) { - return __awaiter(this, void 0, void 0, function* () { - if (this.chapterDocList.length > 0) { - yield this.goToChapter(chapterDocIndex, this.chapterDocList[chapterDocIndex].href, this.chapterDocList[chapterDocIndex].label); - } - }); - } - goToChapter(chapterDocIndex, chapterHref, chapterTitle) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - yield handleRenderChapter(parseInt(chapterDocIndex), chapterTitle, chapterHref, this.chapterDocList, this.element, this.readerMode, this.format, this.tempLocation, doc, iframe); - if (chapterHref && chapterHref.indexOf("#") > -1) { - yield handleScrollPosition(this.element, this.readerMode, "", "", chapterHref, "", doc); - } - yield this.record(); - this.trigger("rendered"); - }); - } - goToPosition(bookLocationStr) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - let bookLocation = JSON.parse(bookLocationStr); - this.tempLocation = { - text: bookLocation.text, - chapterTitle: bookLocation.chapterTitle, - chapterDocIndex: bookLocation.chapterDocIndex, - chapterHref: bookLocation.chapterHref, - count: bookLocation.count, - page: bookLocation.page, - percentage: bookLocation.percentage, - }; - let { text, chapterTitle, chapterDocIndex, chapterHref, count, page, cfi } = bookLocation; - yield handleRenderChapter(parseInt(chapterDocIndex), chapterTitle, chapterHref, this.chapterDocList, this.element, this.readerMode, this.format, this.tempLocation, doc, iframe); - if (cfi) { - const cfiInfo = new CFI(cfi, {}); - let doc = this.getDocument(); - if (!doc) { - return; - } - const { node, offset } = cfiInfo.resolve(doc, {}); - if (node) { - let element = null; - let currentNode = node; - while (currentNode) { - const temp = currentNode; - if (temp.tagName && - "h1,h2,h3,h4,h5,h6,p,div,ul,dl,ol,pre,li,dt,dd,blockquote,address,kookitmarker".indexOf(temp.tagName.toLowerCase()) > -1) { - element = temp; - break; - } - currentNode = currentNode.parentNode; - } - if (element) { - count = "ignore"; - text = element.textContent; - } - } - } - yield handleScrollPosition(this.element, this.readerMode, text, count, "", page, doc); - rangy.init(); - yield this.record(); - this.trigger("rendered"); - // this.addPageAnimation(); - }); - } - getDocument() { - let pageArea = document.getElementById("page-area"); - if (!pageArea) - return null; - let iframe = pageArea.getElementsByTagName("iframe")[0]; - if (!iframe) - return null; - let doc = iframe.contentDocument; - if (!doc) { - return null; - } - return doc; - } - getIframe() { - let pageArea = document.getElementById("page-area"); - if (!pageArea) - return null; - let iframe = pageArea.getElementsByTagName("iframe")[0]; - if (!iframe) - return null; - return iframe; - } - goToNode(node) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc) { - return; - } - let targetNode = getCloestBlock(node, this.element, this.readerMode); - let left = targetNode - ? convertStyleNum(targetNode.offsetLeft) - - convertStyleNum(targetNode.marginLeft || - parseFloat(getComputedStyle(targetNode).marginLeft)) - : 0; - let top = targetNode - ? convertStyleNum(targetNode.offsetTop) - - convertStyleNum(targetNode.marginTop || - parseFloat(getComputedStyle(targetNode).marginTop)) - : 0; - if (this.readerMode !== "scroll") { - doc.body.scrollTo(left, 0); - } - else { - this.element.scrollTo(0, top); - } - yield this.record(); - this.trigger("rendered"); - }); - } - removeContent() { - this.element.innerHTML = ""; - } - prev() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) { - return; - } - if ((this.readerMode === "scroll" && - convertStyleNum(this.element.scrollTop) === 0) || - (this.readerMode !== "scroll" && - convertStyleNum(doc.body.scrollLeft) === 0)) { - if (this.tempLocation.chapterDocIndex === "0") { - return; - } - if (this.animation === "mimical" && - this.readerMode !== "scroll" && - this.isMobile === "yes") { - //sleep 1s prevent animation stuck - yield new Promise((r) => setTimeout(r, 500)); - } - yield handlePrevChapter(this.element, this.flatChapter(this.chapterList), this.chapterDocList, this.readerMode, this.format, this.tempLocation, doc, iframe); - let chapterDocIndex = parseInt(this.tempLocation.chapterDocIndex || "-1"); - if (chapterDocIndex > -1) { - if (this.readerMode === "scroll") { - this.element.scrollTo(0, doc.body.scrollHeight); - } - else { - doc.body.scrollTo(doc.body.scrollWidth, 0); - } - } - this.trigger("rendered"); - } - else if (this.readerMode === "scroll") { - // scroll readerMode under normal condition - this.element.scrollBy({ - left: 0, - top: -(this.element.clientHeight - 50), - behavior: "smooth", - }); - } - else { - yield handleScrollPage(this.element, this.animation, 1, doc, this.flipToNextPage, this.flipToPrevPage, this.isMobile); - } - yield this.record(); - }); - } - next() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) { - return; - } - if ((Math.abs(doc.body.scrollWidth - - convertStyleNum(doc.body.scrollLeft) - - doc.body.clientWidth) < 50 && - this.readerMode !== "scroll") || - (Math.abs(this.element.scrollHeight - - convertStyleNum(this.element.scrollTop) - - this.element.clientHeight) < 20 && - this.readerMode === "scroll")) { - if (this.animation === "mimical" && - this.readerMode !== "scroll" && - this.isMobile === "yes") { - //sleep 1s prevent animation stuck - yield new Promise((r) => setTimeout(r, 500)); - } - // if the last page - yield handleNextChapter(this.element, this.flatChapter(this.chapterList), this.chapterDocList, this.readerMode, this.format, this.tempLocation, doc, iframe); - this.trigger("rendered"); - } - else if (this.readerMode === "scroll") { - // scroll readerMode under normal condition - if (Math.abs(this.element.scrollHeight - - convertStyleNum(this.element.scrollTop) - - this.element.clientHeight) - - (this.element.clientHeight - 50) < - 20 && - Math.abs(this.element.scrollHeight - - convertStyleNum(this.element.scrollTop) - - this.element.clientHeight) > 20) { - this.element.scrollTo({ - left: 0, - top: this.element.scrollHeight - 20, - behavior: "smooth", - }); - } - else { - this.element.scrollBy({ - left: 0, - top: this.element.clientHeight - 50, - behavior: "smooth", - }); - } - } - else { - // single and double readerMode under normal condition - yield handleScrollPage(this.element, this.animation, -1, doc, this.flipToNextPage, this.flipToPrevPage, this.isMobile); - } - yield this.record(); - }); - } - prevChapter() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - yield handlePrevChapter(this.element, this.flatChapter(this.chapterList), this.chapterDocList, this.readerMode, this.format, this.tempLocation, doc, iframe); - yield this.record(); - this.trigger("rendered"); - }); - } - nextChapter() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - yield handleNextChapter(this.element, this.flatChapter(this.chapterList), this.chapterDocList, this.readerMode, this.format, this.tempLocation, doc, iframe); - yield this.record(); - this.trigger("rendered"); - }); - } - visibleText() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc) - return ""; - return getVisibleText(this.element, this.readerMode, doc); - }); - } - audioText() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc) - return ""; - return getAudioText(this.element, this.readerMode, doc); - }); - } - chapterText() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc) - return ""; - return doc.body.textContent || ""; - }); - } - autoScroll(rate, isStart) { - let doc = this.getDocument(); - if (!doc) - return; - if (this.scrollTimer) { - cancelAnimationFrame(this.scrollTimer); - this.scrollTimer = null; - } - if (this.recordTimer) { - clearInterval(this.recordTimer); - this.recordTimer = null; - } - if (isStart === "no" || this.readerMode !== "scroll") { - return; - } - let accumulatedScroll = 0; // 累积滚动量 - let frameCount = 0; // 帧计数器 - const scrollStep = () => { - accumulatedScroll += rate; - frameCount++; - // 对于慢速滚动,使用更精细的控制 - if (Math.abs(rate) < 1) { - // 每隔一定帧数或累积到足够像素时滚动 - const shouldScroll = Math.abs(accumulatedScroll) >= 0.5 || - frameCount % Math.max(1, Math.floor(30 / Math.abs(rate))) === 0; - if (shouldScroll && Math.abs(accumulatedScroll) >= 0.1) { - const scrollAmount = Math.round(accumulatedScroll * 10) / 10; // 保留一位小数 - this.element.scrollBy({ - left: 0, - top: scrollAmount, - behavior: "auto", - }); - accumulatedScroll = 0; // 重置累积量 - frameCount = 0; // 重置帧计数 - } - } - else { - // 快速滚动时保持原有逻辑 - if (Math.abs(accumulatedScroll) >= 1) { - const scrollAmount = Math.floor(accumulatedScroll); - this.element.scrollBy({ - left: 0, - top: scrollAmount, - behavior: "auto", - }); - accumulatedScroll -= scrollAmount; // 减去已滚动的量 - } - } - this.scrollTimer = requestAnimationFrame(scrollStep); - }; - this.scrollTimer = requestAnimationFrame(scrollStep); - this.recordTimer = setInterval(() => { - if (this.readerMode === "scroll" && - Math.abs(this.element.scrollHeight - - this.element.scrollTop - - this.element.clientHeight) < 10) { - this.nextChapter(); - } - this.record(); - }, 3000); - } - autoScrollIOS(rate, isStart) { - let doc = this.getDocument(); - if (!doc) - return; - if (this.scrollTimer) { - clearInterval(this.scrollTimer); - this.scrollTimer = null; - } - if (this.recordTimer) { - clearInterval(this.recordTimer); - this.recordTimer = null; - } - if (isStart === "no" || this.readerMode !== "scroll") { - return; - } - let accumulatedScroll = 0; // 累积滚动量 - let realScrollTop = this.element.scrollTop; // 记录真实滚动位置 - // this.scrollTimer = requestAnimationFrame(scrollStep); - this.scrollTimer = setInterval(() => { - accumulatedScroll += rate; - if (doc) { - doc.body.style.transform = `translateY(-${accumulatedScroll}px)`; - // 每隔一定距离同步真实滚动位置,避免transform累积过大 - if (Math.abs(accumulatedScroll) >= 50) { - // 重置transform - doc.body.style.transform = "translateY(0px)"; - // 更新真实滚动位置 - realScrollTop += accumulatedScroll; - this.element.scrollTo({ - left: 0, - top: realScrollTop, - behavior: "auto", - }); - // 重置累积量 - accumulatedScroll = 0; - } - } - }, 30); - this.recordTimer = setInterval(() => { - if (this.readerMode === "scroll" && - Math.abs(this.element.scrollHeight - - this.element.scrollTop - - this.element.clientHeight) < 10) { - this.nextChapter(); - } - this.record(); - }, 3000); - } - highlightSearchNode(text, style) { - let doc = this.getDocument(); - if (!doc) - return; - handleHighlightSearchNode(text, style, doc); - } - highlightAudioNode(text, style) { - let doc = this.getDocument(); - if (!doc) - return; - handleHighlightAudioNode(text, style, doc, this.element, this.readerMode); - } - doSearch(keyword) { - return __awaiter(this, void 0, void 0, function* () { - if (this.format === "PDF") { - return yield getPDFSearchResult(keyword, this.chapterDocList); - } - else { - return yield getSearchResult(keyword, this.chapterDocList); - } - }); - } - getProgress() { - let doc = this.getDocument(); - if (!doc) - return; - return progressInfo(this.readerMode, doc, this.element); - } - record() { - return __awaiter(this, void 0, void 0, function* () { - if (this.animation !== "") { - yield new Promise((r) => setTimeout(r, 1000)); - } - let doc = this.getDocument(); - if (!doc) - return; - yield handleRecord(this.element, this.readerMode, this.flatChapter(this.chapterList), this.chapterDocList, this.tempLocation, doc, null); - this.trigger("page-changed"); - }); - } - getPosition() { - return this.tempLocation; - } - getNotePosition() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc) - return; - let selectedElement = getSelectedElement(doc); - if (!selectedElement) - return; - yield handleRecord(this.element, this.readerMode, this.flatChapter(this.chapterList), this.chapterDocList, this.tempLocation, doc, selectedElement); - return this.tempLocation; - }); - } - setStyle(css) { - let doc = this.getDocument(); - if (!doc) - return; - var defaultStyle = document.createElement("style"); - defaultStyle.innerHTML = css; - doc.head.appendChild(defaultStyle); - } - getHightlightCoords() { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - let charRange = rangy.getSelection(iframe).saveCharacterRanges(doc.body)[0]; - return charRange; - }); - } - renderHighlighters(notes, handleNoteClick) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - clearHighlight(doc); - // if more than 20 notes, render one by one to avoid blocking the UI thread - for (let index = 0; index < notes.length; index++) { - const item = notes[index]; - try { - yield new Promise((r) => setTimeout(r, 5)); - showNoteHighlight(JSON.parse(item.range), item.color, item.key, handleNoteClick, doc, iframe); - // highlighter.highlightSelection(classes[item.color]); - } - catch (e) { - console.error(e, "Exception has been caught when restore character ranges."); - return; - } - } - }); - } - removeOneNote(key, chapterDocIndex) { - let doc = this.getDocument(); - if (!doc) - return; - const elements = doc.querySelectorAll(".kookit-note"); - for (let index = 0; index < elements.length; index++) { - const element = elements[index]; - const dataKey = element.getAttribute("data-key"); - if (dataKey === key) { - element.parentNode.removeChild(element); - } - } - } - createOneNote(item, handleNoteClick) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - let iframe = this.getIframe(); - if (!doc || !iframe) - return; - showNoteHighlight(JSON.parse(item.range), item.color, item.key, handleNoteClick, doc, iframe); - }); - } - displayFontBase64(fontName, fontBase64, fontFormat, fontType) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc || fontBase64.length === 0) - return; - const font = new FontFace(fontName, `url(data:font/${fontType};charset=utf-8;base64,${fontBase64})`); - let loadedFont = yield font.load(); - // 将加载的字体添加到文档的字体集合中 - document.fonts.add(loadedFont); - const fontFaceCSS = "@font-face {" + - " font-family: '" + - fontName + - "';" + - " src: url('data:font/" + - fontType + - ";charset=utf-8;base64," + - fontBase64 + - "') format('" + - fontFormat + - "');" + - "}"; - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.appendChild(document.createTextNode(fontFaceCSS)); - doc.head.appendChild(styleElement); - }); - } - displayFontUrl(fontName, fontUrl) { - return __awaiter(this, void 0, void 0, function* () { - let doc = this.getDocument(); - if (!doc || fontUrl.length === 0) - return; - // 使用 FontFace API 创建字体 - const font = new FontFace(fontName, `url(${fontUrl})`); - // 加载字体并监听加载完成事件 - let loadedFont = yield font.load(); - // 将加载的字体添加到文档的字体集合中 - document.fonts.add(loadedFont); - const fontFaceCSS = "@font-face {" + - " font-family: '" + - fontName + - "';" + - " src: url('" + - fontUrl + - "') format('truetype');" + - "}"; - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.appendChild(document.createTextNode(fontFaceCSS)); - doc.head.appendChild(styleElement); - }); - } - getAllDocuments() { - let doc = this.getDocument(); - if (!doc) - return []; - if (this.format !== "PDF") { - return [doc]; - } - let iframes = doc.querySelectorAll("iframe"); - let documents = []; - iframes.forEach((iframe) => { - let iframeDoc = iframe.contentDocument; - if (iframeDoc) { - documents.push(iframeDoc); - } - }); - return [doc, ...documents]; - } - getAllIframes() { - let iframe = this.getIframe(); - if (!iframe) - return []; - if (this.format !== "PDF") { - return [iframe]; - } - let doc = this.getDocument(); - if (!doc) - return []; - let iframes = doc.querySelectorAll("iframe"); - let iframeElements = []; - iframes.forEach((iframe) => { - let iframeElement = iframe; - iframeElements.push(iframeElement); - }); - return [iframe, ...iframeElements]; - } - addTouchEvent(isAndroid, touchControlRule) { - let docs = this.getAllDocuments(); - let iframes = this.getAllIframes(); - for (let index = 0; index < docs.length; index++) { - const doc = docs[index]; - const iframe = iframes[index]; - if (!doc || !iframe) - continue; - let iframeId = iframe.id; - if (this.touchEventSet[iframeId]) { - continue; - } - this.touchEventSet[iframeId] = true; - if (isAndroid === "yes") { - addAndroidTouchEvent(doc, iframe, this.element, this.readerMode, this.animation, this.format, touchControlRule, this); - } - else { - addAppleTouchEvent(doc, iframe, this.element, this.readerMode, this.animation, this.format, touchControlRule, this); - } - } - } - clearSelection() { - var _a, _b; - let iframes = this.getAllIframes(); - for (let index = 0; index < iframes.length; index++) { - const iframe = iframes[index]; - if (!iframe) - continue; - let iWin = iframe.contentWindow || ((_a = iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.defaultView); - if (!iWin || !iWin.getSelection()) - return; - (_b = iWin.getSelection()) === null || _b === void 0 ? void 0 : _b.empty(); - } - } - restoreSelectionClearHighlight() { - if (this.format === "PDF") { - return; - } - let doc = this.getDocument(); - if (!doc) - return; - let tempHighlights = doc.querySelectorAll("#temp-highlight"); - tempHighlights.forEach((element) => { - var _a; - (_a = element.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(element); - }); - let iframe = this.getIframe(); - if (!iframe) - return; - let charRange = window.charRange; - if (!charRange) - return; - rangy.getSelection(iframe).restoreCharacterRanges(doc, [charRange]); - } -} - -const mimetype = { - svg: "image/svg+xml", - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - zip: "application/zip", - rar: "application/x-rar-compressed", - "7z": "application/x-7z-compressed", - tar: "application/x-tar", - html: "text/html", - htm: "text/html", - xml: "text/xml", - xhtml: "application/xhtml+xml", - css: "text/css", -}; -const mimetypeReverse = { - "image/svg+xml": "svg", - "image/png": "png", - "image/jpeg": "jpg", - "image/gif": "gif", - "image/webp": "webp", - "application/zip": "zip", - "application/x-rar-compressed": "rar", - "application/x-7z-compressed": "7z", - "application/x-tar": "tar", - "text/html": "html", - "text/xml": "xml", - "application/xhtml+xml": "xhtml", - "text/css": "css", -}; - -const makeCacheBook = (bookBuffer) => __awaiter(void 0, void 0, void 0, function* () { - let zip = yield JSZip.loadAsync(bookBuffer); - var tocZip = zip.file("toc.json"); - let toc = []; - if (tocZip) { - toc = JSON.parse(yield tocZip.async("string")); - } - var sectionsZip = zip.file("sections.json"); - let sections = []; - if (sectionsZip) { - sections = JSON.parse(yield sectionsZip.async("string")); - } - const load = (index) => __awaiter(void 0, void 0, void 0, function* () { - var chapterZip = zip.file("chapters/" + index + ".html"); - let chapter = ""; - if (chapterZip) { - chapter = yield chapterZip.async("string"); - } - return URL.createObjectURL(new Blob([chapter], { type: "text/html" })); - }); - const unload = (index) => { }; - const book = {}; - book.getCover = () => ""; - const loadAsset = (url) => __awaiter(void 0, void 0, void 0, function* () { - var assetZip = zip.file(url); - let asset; - if (assetZip) { - asset = yield assetZip.async("arraybuffer"); - } - return URL.createObjectURL(new Blob([asset], { type: mimetype[url.split(".").reverse()[0]] })); - }); - book.sections = sections.map((item, index) => ({ - id: item.href, - load: () => load(index), - unload: () => unload(), - loadAsset: (url) => loadAsset(url), - })); - book.toc = toc.map((item) => ({ - label: item.label, - href: item.href, - subitems: item.subitems, - })); - book.rendition = { layout: "pre-paginated" }; - book.resolveHref = (href) => { - return { index: _.findLastIndex(sections, { href }) }; - }; - book.splitTOCHref = (href) => [href, null]; - book.getTOCFragment = (doc) => doc.documentElement; - return book; -}); -const getCache = (book) => { - return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () { - let parser = new GeneralParser(book); - let chapterList = yield parser.getChapter(book.toc); - let chapterDocList = yield parser.getChapterDoc(); - let toc = chapterList; - let sections = chapterDocList.map((item) => { - return { href: item.href, label: item.label }; - }); - let chapterTexts = yield Promise.all(chapterDocList.map((item) => __awaiter(void 0, void 0, void 0, function* () { - let chapterText = ""; - if (item.text && item.text.load) { - let blob = yield fetch(yield item.text.load()).then((r) => r.blob()); - chapterText = yield blob.text(); - } - return chapterText; - }))); - let zip = new JSZip(); - zip.file("toc.json", JSON.stringify(toc)); - zip.file("sections.json", JSON.stringify(sections)); - let chapters = []; - //todo get css, fonts and images blob - for (let index = 0; index < chapterTexts.length; index++) { - let chapterDoc = new DOMParser().parseFromString(chapterTexts[index], "text/html"); - let imgDomList = getImageElement(chapterDoc); - for (let subindex = 0; subindex < imgDomList.length; subindex++) { - let subImgZip = zip.folder("imgs/" + index); - if (!subImgZip) { - break; - } - let imageUrl = imgDomList[subindex].getAttribute("src") || - imgDomList[subindex].getAttribute("xlink:href"); - if (imageUrl) { - try { - let blob = yield fetch(yield imageUrl).then((r) => r.blob()); - subImgZip.file(subindex + "." + mimetypeReverse[blob.type], blob); - let newUrl = "imgs/" + - index + - "/" + - subindex + - "." + - mimetypeReverse[blob.type]; - imgDomList[subindex].src = newUrl; - if (imgDomList[subindex].getAttribute("xlink:href")) { - imgDomList[subindex].setAttribute("xlink:href", newUrl); - } - } - catch (error) { - console.error(error); - } - } - } - let linkList = Array.from(chapterDoc.getElementsByTagName("link")); - for (let subindex = 0; subindex < linkList.length; subindex++) { - let link = linkList[subindex]; - let subCssZip = zip.folder("css/" + index); - if (!subCssZip) { - break; - } - if (link.getAttribute("href")) { - try { - let blob = yield fetch(yield link.getAttribute("href")).then((r) => r.blob()); - subCssZip.file(subindex + "." + mimetypeReverse[blob.type], blob); - link.href = - "css/" + - index + - "/" + - subindex + - "." + - mimetypeReverse[blob.type]; - } - catch (error) { - console.error(error); - } - } - } - chapters.push(chapterDoc.documentElement.innerHTML); - } - let configZip = zip.folder("chapters"); - if (!configZip) { - return; - } - for (let index = 0; index < chapters.length; index++) { - configZip.file(index + ".html", chapters[index]); - } - zip - .generateAsync({ type: "blob" }) - .then((blob) => __awaiter(void 0, void 0, void 0, function* () { - resolve(yield new Response(blob).arrayBuffer()); - })) - .catch((err) => { - resolve("err"); - }); - })); -}; - -class EpubRender extends GeneralRender { - constructor(epubBuffer, config) { - super(Object.assign(Object.assign({}, config), { format: "EPUB" })); - this.epubBuffer = epubBuffer; - } - renderTo(element) { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - this.element = element; - if (!this.book) { - yield this.parse(); - } - let parser = new GeneralParser(this.book); - this.chapterList = yield parser.getChapter(this.book.toc); - this.chapterDocList = yield parser.getChapterDoc(); - createIframe(element); - let doc = this.getDocument(); - if (!doc) - return; - handleLayout(element, this.readerMode, doc); - resolve(); - })); - } - parse() { - return __awaiter(this, void 0, void 0, function* () { - let blob = new Blob([this.epubBuffer]); - let file = new File([blob], "book", { - lastModified: new Date().getTime(), - type: blob.type, - }); - try { - const loader = yield this.makeZipLoader(file); - this.book = yield new EPUB(loader).init(); - } - catch (error) { - console.error(error); - throw error; - } - }); - } - preCache() { - return __awaiter(this, void 0, void 0, function* () { - try { - if (!this.book) { - yield this.parse(); - } - return yield getCache(this.book); - } - catch (error) { - return ""; - } - }); - } - makeZipLoader(file) { - return __awaiter(this, void 0, void 0, function* () { - let zip = yield JSZip.loadAsync(file); - const entries = zip.files; - const loadText = (name) => __awaiter(this, void 0, void 0, function* () { - let entry = zip.file(name); - if (entry) { - return entry.async("string"); - } - return ""; - }); - const loadBlob = (name) => __awaiter(this, void 0, void 0, function* () { - let entry = zip.file(name); - if (entry) { - let buffer = yield entry.async("arraybuffer"); - return new Blob([buffer]); - } - return new Blob([new ArrayBuffer(0)]); - }); - const getSize = (name) => { - let entry = zip.file(name); - if (entry) { - return entry._data.uncompressedSize || 0; - } - }; - return { - entries: Object.values(entries).map((item) => { - return { filename: item.name }; - }), - loadText, - loadBlob, - getSize, - }; - }); - } - getMetadata() { - return __awaiter(this, void 0, void 0, function* () { - try { - if (!this.book) { - yield this.parse(); - } - let parser = new GeneralParser(this.book); - return yield parser.getMetadata(); - } - catch (error) { - console.error(error, "error"); - throw error; - } - }); - } -} - -const unescapeHTML = (str) => { - if (!str) return ""; - const textarea = document.createElement("textarea"); - textarea.innerHTML = str; - return textarea.value; -}; - -const MIME$1 = { - XML: "application/xml", - XHTML: "application/xhtml+xml", - HTML: "text/html", - CSS: "text/css", - SVG: "image/svg+xml", -}; - -const PDB_HEADER = { - name: [0, 32, "string"], - type: [60, 4, "string"], - creator: [64, 4, "string"], - numRecords: [76, 2, "uint"], -}; - -const PALMDOC_HEADER = { - compression: [0, 2, "uint"], - numTextRecords: [8, 2, "uint"], - recordSize: [10, 2, "uint"], - encryption: [12, 2, "uint"], -}; - -const MOBI_HEADER = { - magic: [16, 4, "string"], - length: [20, 4, "uint"], - type: [24, 4, "uint"], - encoding: [28, 4, "uint"], - uid: [32, 4, "uint"], - version: [36, 4, "uint"], - titleOffset: [84, 4, "uint"], - titleLength: [88, 4, "uint"], - localeRegion: [94, 1, "uint"], - localeLanguage: [95, 1, "uint"], - resourceStart: [108, 4, "uint"], - huffcdic: [112, 4, "uint"], - numHuffcdic: [116, 4, "uint"], - exthFlag: [128, 4, "uint"], - trailingFlags: [240, 4, "uint"], - indx: [244, 4, "uint"], -}; - -const KF8_HEADER = { - resourceStart: [108, 4, "uint"], - fdst: [192, 4, "uint"], - numFdst: [196, 4, "uint"], - frag: [248, 4, "uint"], - skel: [252, 4, "uint"], - guide: [260, 4, "uint"], -}; - -const EXTH_HEADER = { - magic: [0, 4, "string"], - length: [4, 4, "uint"], - count: [8, 4, "uint"], -}; - -const INDX_HEADER = { - magic: [0, 4, "string"], - length: [4, 4, "uint"], - type: [8, 4, "uint"], - idxt: [20, 4, "uint"], - numRecords: [24, 4, "uint"], - encoding: [28, 4, "uint"], - language: [32, 4, "uint"], - total: [36, 4, "uint"], - ordt: [40, 4, "uint"], - ligt: [44, 4, "uint"], - numLigt: [48, 4, "uint"], - numCncx: [52, 4, "uint"], -}; - -const TAGX_HEADER = { - magic: [0, 4, "string"], - length: [4, 4, "uint"], - numControlBytes: [8, 4, "uint"], -}; - -const HUFF_HEADER = { - magic: [0, 4, "string"], - offset1: [8, 4, "uint"], - offset2: [12, 4, "uint"], -}; - -const CDIC_HEADER = { - magic: [0, 4, "string"], - length: [4, 4, "uint"], - numEntries: [8, 4, "uint"], - codeLength: [12, 4, "uint"], -}; - -const FDST_HEADER = { - magic: [0, 4, "string"], - numEntries: [8, 4, "uint"], -}; - -const FONT_HEADER = { - flags: [8, 4, "uint"], - dataStart: [12, 4, "uint"], - keyLength: [16, 4, "uint"], - keyStart: [20, 4, "uint"], -}; - -const MOBI_ENCODING = { - 1252: "windows-1252", - 65001: "utf-8", -}; - -const EXTH_RECORD_TYPE = { - 100: ["creator", "string", true], - 101: ["publisher"], - 103: ["description"], - 104: ["isbn"], - 105: ["subject", "string", true], - 106: ["date"], - 108: ["contributor", "string", true], - 109: ["rights"], - 110: ["subjectCode", "string", true], - 112: ["source", "string", true], - 113: ["asin"], - 121: ["boundary", "uint"], - 122: ["fixedLayout"], - 125: ["numResources", "uint"], - 126: ["originalResolution"], - 127: ["zeroGutter"], - 128: ["zeroMargin"], - 129: ["coverURI"], - 132: ["regionMagnification"], - 201: ["coverOffset", "uint"], - 202: ["thumbnailOffset", "uint"], - 503: ["title"], - 524: ["language", "string", true], - 527: ["pageProgressionDirection"], -}; - -const MOBI_LANG = { - 1: [ - "ar", - "ar-SA", - "ar-IQ", - "ar-EG", - "ar-LY", - "ar-DZ", - "ar-MA", - "ar-TN", - "ar-OM", - "ar-YE", - "ar-SY", - "ar-JO", - "ar-LB", - "ar-KW", - "ar-AE", - "ar-BH", - "ar-QA", - ], - 2: ["bg"], - 3: ["ca"], - 4: ["zh", "zh-TW", "zh-CN", "zh-HK", "zh-SG"], - 5: ["cs"], - 6: ["da"], - 7: ["de", "de-DE", "de-CH", "de-AT", "de-LU", "de-LI"], - 8: ["el"], - 9: [ - "en", - "en-US", - "en-GB", - "en-AU", - "en-CA", - "en-NZ", - "en-IE", - "en-ZA", - "en-JM", - null, - "en-BZ", - "en-TT", - "en-ZW", - "en-PH", - ], - 10: [ - "es", - "es-ES", - "es-MX", - null, - "es-GT", - "es-CR", - "es-PA", - "es-DO", - "es-VE", - "es-CO", - "es-PE", - "es-AR", - "es-EC", - "es-CL", - "es-UY", - "es-PY", - "es-BO", - "es-SV", - "es-HN", - "es-NI", - "es-PR", - ], - 11: ["fi"], - 12: ["fr", "fr-FR", "fr-BE", "fr-CA", "fr-CH", "fr-LU", "fr-MC"], - 13: ["he"], - 14: ["hu"], - 15: ["is"], - 16: ["it", "it-IT", "it-CH"], - 17: ["ja"], - 18: ["ko"], - 19: ["nl", "nl-NL", "nl-BE"], - 20: ["no", "nb", "nn"], - 21: ["pl"], - 22: ["pt", "pt-BR", "pt-PT"], - 23: ["rm"], - 24: ["ro"], - 25: ["ru"], - 26: ["hr", null, "sr"], - 27: ["sk"], - 28: ["sq"], - 29: ["sv", "sv-SE", "sv-FI"], - 30: ["th"], - 31: ["tr"], - 32: ["ur"], - 33: ["id"], - 34: ["uk"], - 35: ["be"], - 36: ["sl"], - 37: ["et"], - 38: ["lv"], - 39: ["lt"], - 41: ["fa"], - 42: ["vi"], - 43: ["hy"], - 44: ["az"], - 45: ["eu"], - 46: ["hsb"], - 47: ["mk"], - 48: ["st"], - 49: ["ts"], - 50: ["tn"], - 52: ["xh"], - 53: ["zu"], - 54: ["af"], - 55: ["ka"], - 56: ["fo"], - 57: ["hi"], - 58: ["mt"], - 59: ["se"], - 62: ["ms"], - 63: ["kk"], - 65: ["sw"], - 67: ["uz", null, "uz-UZ"], - 68: ["tt"], - 69: ["bn"], - 70: ["pa"], - 71: ["gu"], - 72: ["or"], - 73: ["ta"], - 74: ["te"], - 75: ["kn"], - 76: ["ml"], - 77: ["as"], - 78: ["mr"], - 79: ["sa"], - 82: ["cy", "cy-GB"], - 83: ["gl", "gl-ES"], - 87: ["kok"], - 97: ["ne"], - 98: ["fy"], -}; - -const concatTypedArray = (a, b) => { - const result = new a.constructor(a.length + b.length); - result.set(a); - result.set(b, a.length); - return result; -}; -const concatTypedArray3 = (a, b, c) => { - const result = new a.constructor(a.length + b.length + c.length); - result.set(a); - result.set(b, a.length); - result.set(c, a.length + b.length); - return result; -}; - -const decoder = new TextDecoder(); -const getString = (buffer) => decoder.decode(buffer); -const getUint = (buffer) => { - if (!buffer) return; - const l = buffer.byteLength; - const func = l === 4 ? "getUint32" : l === 2 ? "getUint16" : "getUint8"; - return new DataView(buffer)[func](0); -}; -const getStruct = (def, buffer) => - Object.fromEntries( - Array.from(Object.entries(def)).map(([key, [start, len, type]]) => [ - key, - (type === "string" ? getString : getUint)( - buffer.slice(start, start + len) - ), - ]) - ); - -const getDecoder = (x) => new TextDecoder(MOBI_ENCODING[x]); - -const getVarLen = (byteArray, i = 0) => { - let value = 0, - length = 0; - for (const byte of byteArray.subarray(i, i + 4)) { - value = (value << 7) | ((byte & 0b111_1111) >>> 0); - length++; - if (byte & 0b1000_0000) break; - } - return { value, length }; -}; - -// variable-length quantity, but read from the end of data -const getVarLenFromEnd = (byteArray) => { - let value = 0; - for (const byte of byteArray.subarray(-4)) { - // `byte & 0b1000_0000` indicates the start of value - if (byte & 0b1000_0000) value = 0; - value = (value << 7) | (byte & 0b111_1111); - } - return value; -}; - -const countBitsSet = (x) => { - let count = 0; - for (; x > 0; x = x >> 1) if ((x & 1) === 1) count++; - return count; -}; - -const countUnsetEnd = (x) => { - let count = 0; - while ((x & 1) === 0) (x = x >> 1), count++; - return count; -}; - -const decompressPalmDOC = (array) => { - let output = []; - for (let i = 0; i < array.length; i++) { - const byte = array[i]; - if (byte === 0) output.push(0); // uncompressed literal, just copy it - else if (byte <= 8) - // copy next 1-8 bytes - for (const x of array.subarray(i + 1, (i += byte) + 1)) output.push(x); - else if (byte <= 0b0111_1111) output.push(byte); // uncompressed literal - else if (byte <= 0b1011_1111) { - // 1st and 2nd bits are 10, meaning this is a length-distance pair - // read next byte and combine it with current byte - const bytes = (byte << 8) | array[i++ + 1]; - // the 3rd to 13th bits encode distance - const distance = (bytes & 0b0011_1111_1111_1111) >>> 3; - // the last 3 bits, plus 3, is the length to copy - const length = (bytes & 0b111) + 3; - for (let j = 0; j < length; j++) - output.push(output[output.length - distance]); - } - // compressed from space plus char - else output.push(32, byte ^ 0b1000_0000); - } - return Uint8Array.from(output); -}; - -const read32Bits = (byteArray, from) => { - const startByte = from >> 3; - const end = from + 32; - const endByte = end >> 3; - let bits = 0n; - for (let i = startByte; i <= endByte; i++) - bits = (bits << 8n) | BigInt(byteArray[i] ?? 0); - return (bits >> (8n - BigInt(end & 7))) & 0xffffffffn; -}; - -const huffcdic = async (mobi, loadRecord) => { - const huffRecord = await loadRecord(mobi.huffcdic); - const { magic, offset1, offset2 } = getStruct(HUFF_HEADER, huffRecord); - if (magic !== "HUFF") throw new Error("Invalid HUFF record"); - - // table1 is indexed by byte value - const table1 = Array.from({ length: 256 }, (_, i) => offset1 + i * 4) - .map((offset) => getUint(huffRecord.slice(offset, offset + 4))) - .map((x) => [x & 0b1000_0000, x & 0b1_1111, x >>> 8]); - - // table2 is indexed by code length - const table2 = [null].concat( - Array.from({ length: 32 }, (_, i) => offset2 + i * 8).map((offset) => [ - getUint(huffRecord.slice(offset, offset + 4)), - getUint(huffRecord.slice(offset + 4, offset + 8)), - ]) - ); - - const dictionary = []; - for (let i = 1; i < mobi.numHuffcdic; i++) { - const record = await loadRecord(mobi.huffcdic + i); - const cdic = getStruct(CDIC_HEADER, record); - if (cdic.magic !== "CDIC") throw new Error("Invalid CDIC record"); - // `numEntries` is the total number of dictionary data across CDIC records - // so `n` here is the number of entries in *this* record - const n = Math.min( - 1 << cdic.codeLength, - cdic.numEntries - dictionary.length - ); - const buffer = record.slice(cdic.length); - for (let i = 0; i < n; i++) { - const offset = getUint(buffer.slice(i * 2, i * 2 + 2)); - const x = getUint(buffer.slice(offset, offset + 2)); - const length = x & 0x7fff; - const decompressed = x & 0x8000; - const value = new Uint8Array( - buffer.slice(offset + 2, offset + 2 + length) - ); - dictionary.push([value, decompressed]); - } - } - - const decompress = (byteArray) => { - let output = new Uint8Array(); - const bitLength = byteArray.byteLength * 8; - for (let i = 0; i < bitLength;) { - const bits = Number(read32Bits(byteArray, i)); - let [found, codeLength, value] = table1[bits >>> 24]; - if (!found) { - while (bits >>> (32 - codeLength) < table2[codeLength][0]) - codeLength += 1; - value = table2[codeLength][1]; - } - if ((i += codeLength) > bitLength) break; - - const code = value - (bits >>> (32 - codeLength)); - let [result, decompressed] = dictionary[code]; - if (!decompressed) { - // the result is itself compressed - result = decompress(result); - // cache the result for next time - dictionary[code] = [result, true]; - } - output = concatTypedArray(output, result); - } - return output; - }; - return decompress; -}; - -const getIndexData = async (indxIndex, loadRecord) => { - const indxRecord = await loadRecord(indxIndex); - const indx = getStruct(INDX_HEADER, indxRecord); - if (indx.magic !== "INDX") throw new Error("Invalid INDX record"); - const decoder = getDecoder(indx.encoding); - - const tagxBuffer = indxRecord.slice(indx.length); - const tagx = getStruct(TAGX_HEADER, tagxBuffer); - if (tagx.magic !== "TAGX") throw new Error("Invalid TAGX section"); - const numTags = (tagx.length - 12) / 4; - const tagTable = Array.from( - { length: numTags }, - (_, i) => new Uint8Array(tagxBuffer.slice(12 + i * 4, 12 + i * 4 + 4)) - ); - - const cncx = {}; - let cncxRecordOffset = 0; - for (let i = 0; i < indx.numCncx; i++) { - const record = await loadRecord(indxIndex + indx.numRecords + i + 1); - const array = new Uint8Array(record); - for (let pos = 0; pos < array.byteLength;) { - const index = pos; - const { value, length } = getVarLen(array, pos); - pos += length; - const result = record.slice(pos, pos + value); - pos += value; - cncx[cncxRecordOffset + index] = decoder.decode(result); - } - cncxRecordOffset += 0x10000; - } - - const table = []; - for (let i = 0; i < indx.numRecords; i++) { - const record = await loadRecord(indxIndex + 1 + i); - const array = new Uint8Array(record); - const indx = getStruct(INDX_HEADER, record); - if (indx.magic !== "INDX") throw new Error("Invalid INDX record"); - for (let j = 0; j < indx.numRecords; j++) { - const offsetOffset = indx.idxt + 4 + 2 * j; - const offset = getUint(record.slice(offsetOffset, offsetOffset + 2)); - - const length = getUint(record.slice(offset, offset + 1)); - const name = getString(record.slice(offset + 1, offset + 1 + length)); - - const tags = []; - const startPos = offset + 1 + length; - let controlByteIndex = 0; - let pos = startPos + tagx.numControlBytes; - for (const [tag, numValues, mask, end] of tagTable) { - if (end & 1) { - controlByteIndex++; - continue; - } - const offset = startPos + controlByteIndex; - const value = getUint(record.slice(offset, offset + 1)) & mask; - if (value === mask) { - if (countBitsSet(mask) > 1) { - const { value, length } = getVarLen(array, pos); - tags.push([tag, null, value, numValues]); - pos += length; - } else tags.push([tag, 1, null, numValues]); - } else tags.push([tag, value >> countUnsetEnd(mask), null, numValues]); - } - - const tagMap = {}; - for (const [tag, valueCount, valueBytes, numValues] of tags) { - const values = []; - if (valueCount != null) { - for (let i = 0; i < valueCount * numValues; i++) { - const { value, length } = getVarLen(array, pos); - values.push(value); - pos += length; - } - } else { - let count = 0; - while (count < valueBytes) { - const { value, length } = getVarLen(array, pos); - values.push(value); - pos += length; - count += length; - } - } - tagMap[tag] = values; - } - table.push({ name, tagMap }); - } - } - return { table, cncx }; -}; - -const getNCX = async (indxIndex, loadRecord) => { - const { table, cncx } = await getIndexData(indxIndex, loadRecord); - const items = table.map(({ tagMap }, index) => ({ - index, - offset: tagMap[1]?.[0], - size: tagMap[2]?.[0], - label: cncx[tagMap[3]] ?? "", - headingLevel: tagMap[4]?.[0], - pos: tagMap[6], - parent: tagMap[21]?.[0], - firstChild: tagMap[22]?.[0], - lastChild: tagMap[23]?.[0], - })); - const getChildren = (item) => { - if (item.firstChild == null) return item; - item.children = items - .filter((x) => x.parent === item.index) - .map(getChildren); - return item; - }; - return items.filter((item) => item.headingLevel === 0).map(getChildren); -}; - -const getEXTH = (buf, encoding) => { - const { magic, count } = getStruct(EXTH_HEADER, buf); - if (magic !== "EXTH") throw new Error("Invalid EXTH header"); - const decoder = getDecoder(encoding); - const results = {}; - let offset = 12; - for (let i = 0; i < count; i++) { - const type = getUint(buf.slice(offset, offset + 4)); - const length = getUint(buf.slice(offset + 4, offset + 8)); - if (type in EXTH_RECORD_TYPE) { - const [name, typ, many] = EXTH_RECORD_TYPE[type]; - const data = buf.slice(offset + 8, offset + length); - const value = typ === "uint" ? getUint(data) : decoder.decode(data); - if (many) { - results[name] ??= []; - results[name].push(value); - } else results[name] = value; - } - offset += length; - } - return results; -}; - -const getFont = async (buf, unzlib) => { - const { flags, dataStart, keyLength, keyStart } = getStruct(FONT_HEADER, buf); - const array = new Uint8Array(buf.slice(dataStart)); - // deobfuscate font - if (flags & 0b10) { - const bytes = keyLength === 16 ? 1024 : 1040; - const key = new Uint8Array(buf.slice(keyStart, keyStart + keyLength)); - const length = Math.min(bytes, array.length); - for (var i = 0; i < length; i++) array[i] = array[i] ^ key[i % key.length]; - } - // decompress font - if (flags & 1) - try { - return await unzlib(array); - } catch (e) { - console.warn(e); - console.warn("Failed to decompress font"); - } - return array; -}; - -const isMOBI = async (file) => { - const magic = getString(await file.slice(60, 68).arrayBuffer()); - return magic === "BOOKMOBI"; // || magic === 'TEXtREAd' -}; - -class PDB { - #file; - #offsets; - pdb; - async open(file) { - this.#file = file; - const pdb = getStruct(PDB_HEADER, await file.slice(0, 78).arrayBuffer()); - this.pdb = pdb; - const buffer = await file.slice(78, 78 + pdb.numRecords * 8).arrayBuffer(); - // get start and end offsets for each record - this.#offsets = Array.from({ length: pdb.numRecords }, (_, i) => - getUint(buffer.slice(i * 8, i * 8 + 4)) - ).map((x, i, a) => [x, a[i + 1]]); - } - loadRecord(index) { - const offsets = this.#offsets[index]; - if (!offsets) throw new RangeError("Record index out of bounds"); - return this.#file.slice(...offsets).arrayBuffer(); - } - async loadMagic(index) { - const start = this.#offsets[index][0]; - return getString(await this.#file.slice(start, start + 4).arrayBuffer()); - } -} - -class MOBI extends PDB { - #start = 0; - #resourceStart; - #decoder; - #encoder; - #decompress; - #removeTrailingEntries; - constructor({ unzlib }) { - super(); - this.unzlib = unzlib; - } - async open(file) { - await super.open(file); - // TODO: if (this.pdb.type === 'TEXt') - this.headers = this.#getHeaders(await super.loadRecord(0)); - this.#resourceStart = this.headers.mobi.resourceStart; - let isKF8 = this.headers.mobi.version >= 8; - if (!isKF8) { - const boundary = this.headers.exth?.boundary; - if (boundary < 0xffffffff) - try { - // it's a "combo" MOBI/KF8 file; try to open the KF8 part - this.headers = this.#getHeaders(await super.loadRecord(boundary)); - this.#start = boundary; - isKF8 = true; - } catch (e) { - console.warn(e); - console.warn("Failed to open KF8; falling back to MOBI"); - } - } - await this.#setup(); - return isKF8 ? new KF8(this).init() : new MOBI6(this).init(); - } - #getHeaders(buf) { - const palmdoc = getStruct(PALMDOC_HEADER, buf); - const mobi = getStruct(MOBI_HEADER, buf); - if (mobi.magic !== "MOBI") throw new Error("Missing MOBI header"); - - const { titleOffset, titleLength, localeLanguage, localeRegion } = mobi; - mobi.title = buf.slice(titleOffset, titleOffset + titleLength); - const lang = MOBI_LANG[localeLanguage]; - mobi.language = lang?.[localeRegion >> 2] ?? lang?.[0]; - - const exth = - mobi.exthFlag & 0b100_0000 - ? getEXTH(buf.slice(mobi.length + 16), mobi.encoding) - : null; - const kf8 = mobi.version >= 8 ? getStruct(KF8_HEADER, buf) : null; - return { palmdoc, mobi, exth, kf8 }; - } - async #setup() { - const { palmdoc, mobi } = this.headers; - this.#decoder = getDecoder(mobi.encoding); - // `TextEncoder` only supports UTF-8 - // we are only encoding ASCII anyway, so I think it's fine - this.#encoder = new TextEncoder(); - - // set up decompressor - const { compression } = palmdoc; - this.#decompress = - compression === 1 - ? (f) => f - : compression === 2 - ? decompressPalmDOC - : compression === 17480 - ? await huffcdic(mobi, this.loadRecord.bind(this)) - : null; - if (!this.#decompress) throw new Error("Unknown compression type"); - - // set up function for removing trailing bytes - const { trailingFlags } = mobi; - const multibyte = trailingFlags & 1; - const numTrailingEntries = countBitsSet(trailingFlags >>> 1); - this.#removeTrailingEntries = (array) => { - for (let i = 0; i < numTrailingEntries; i++) { - const length = getVarLenFromEnd(array); - array = array.subarray(0, -length); - } - if (multibyte) { - const length = (array[array.length - 1] & 0b11) + 1; - array = array.subarray(0, -length); - } - return array; - }; - } - decode(...args) { - return this.#decoder.decode(...args); - } - encode(...args) { - return this.#encoder.encode(...args); - } - loadRecord(index) { - return super.loadRecord(this.#start + index); - } - loadMagic(index) { - return super.loadMagic(this.#start + index); - } - loadText(index) { - return this.loadRecord(index + 1) - .then((buf) => new Uint8Array(buf)) - .then(this.#removeTrailingEntries) - .then(this.#decompress); - } - async loadResource(index) { - const buf = await super.loadRecord(this.#resourceStart + index); - const magic = getString(buf.slice(0, 4)); - if (magic === "FONT") return getFont(buf, this.unzlib); - if (magic === "VIDE" || magic === "AUDI") return buf.slice(12); - return buf; - } - getNCX() { - const index = this.headers.mobi.indx; - if (index < 0xffffffff) return getNCX(index, this.loadRecord.bind(this)); - } - getMetadata() { - const { mobi, exth } = this.headers; - return { - identifier: mobi.uid.toString(), - title: unescapeHTML(exth?.title || this.decode(mobi.title)), - author: exth?.creator?.map(unescapeHTML), - publisher: unescapeHTML(exth?.publisher), - language: exth?.language ?? mobi.language, - published: exth?.date, - description: unescapeHTML(exth?.description), - subject: exth?.subject?.map(unescapeHTML), - rights: unescapeHTML(exth?.rights), - }; - } - async getCover() { - const { exth } = this.headers; - const offset = - exth?.coverOffset < 0xffffffff - ? exth?.coverOffset - : exth?.thumbnailOffset < 0xffffffff - ? exth?.thumbnailOffset - : null; - if (offset != null) { - const buf = await this.loadResource(offset); - return new Blob([buf]); - } - } -} - -const mbpPagebreakRegex = /<\s*(?:mbp:)?pagebreak[^>]*>/gi; -const fileposRegex = /<[^<>]+filepos=['"]{0,1}(\d+)[^<>]*>/gi; - -const getIndent = (el) => { - let x = 0; - while (el) { - const parent = el.parentElement; - if (parent) { - const tag = parent.tagName.toLowerCase(); - if (tag === "p") x += 1.5; - else if (tag === "blockquote") x += 2; - } - el = parent; - } - return x; -}; - -class MOBI6 { - parser = new DOMParser(); - serializer = new XMLSerializer(); - #resourceCache = new Map(); - #textCache = new Map(); - #cache = new Map(); - #sections; - #fileposList = []; - #type = MIME$1.HTML; - constructor(mobi) { - this.mobi = mobi; - } - async init() { - // load all text records in an array - let array = new Uint8Array(); - for (let i = 0; i < this.mobi.headers.palmdoc.numTextRecords; i++) - array = concatTypedArray(array, await this.mobi.loadText(i)); - - // convert to string so we can use regex - // note that `filepos` are byte offsets - // so it needs to preserve each byte as a separate character - // (see https://stackoverflow.com/q/50198017) - const str = Array.from(new Uint8Array(array), (c) => - String.fromCharCode(c) - ).join(""); - - // split content into sections at each `${para}
`).join("")}${para.text.trim()}
`) - .join("") - : "Empty"}${e}
`)).join("")}${e.text.trim()}
`)).join(""):"Empty"}Chapter ${t}
`)}for(const e of a){const t=Re(e);t&&c.has(t)?n.push(`${e}
`)}}else for(const e of r){const i=Re(e);i&&Pe(i,t)?n.push(`${e}
`)}return n.join("")||`${e}
`})(e,i,r):e,"text/html");let o=Vi(n);0===o.length&&(o=_i(n));for(let e=0;e