2023-05-15 12:09:18 +00:00
|
|
|
class EpubParser {
|
|
|
|
constructor(filesList) {
|
|
|
|
this.files = filesList;
|
|
|
|
this.parser = new DOMParser();
|
|
|
|
this.opfXml = this.getOPFXml();
|
2023-05-15 15:10:07 +00:00
|
|
|
this.encoder = new TextEncoder();
|
2023-05-15 12:09:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-05-22 21:36:14 +00:00
|
|
|
getTotalByteLength() { //TODO unrealistic values
|
2023-05-15 12:09:18 +00:00
|
|
|
let size = 0;
|
2023-05-15 12:36:33 +00:00
|
|
|
for (let key of Object.keys(this.files)) {
|
|
|
|
let file = this.files[key];
|
2023-05-15 12:09:18 +00:00
|
|
|
if (file.name.endsWith("html")) {
|
2023-05-21 20:23:53 +00:00
|
|
|
// console.log(file.name + " " + file._data.uncompressedSize)
|
2023-05-15 12:09:18 +00:00
|
|
|
size += file._data.uncompressedSize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-05-18 20:37:09 +00:00
|
|
|
* gets file from files and returns decompressed content as string
|
2023-05-15 12:09:18 +00:00
|
|
|
* @param {string} filename name of the file in filelist
|
|
|
|
* @return {string} string representation of decompressed bytes
|
|
|
|
*/
|
|
|
|
decompress(filename) {
|
|
|
|
return pako.inflate(this.files[filename]._data.compressedContent, {raw: true, to: "string"});
|
|
|
|
}
|
|
|
|
|
|
|
|
getOPFXml() {
|
|
|
|
let content = this.decompress("META-INF/container.xml");
|
|
|
|
let xml = this.parser.parseFromString(content, "text/xml");
|
|
|
|
let path = xml.getElementsByTagName("rootfile")[0].getAttribute("full-path");
|
|
|
|
this.opfDir = path.split("/").slice(0, -1).join("/");
|
|
|
|
return this.parser.parseFromString(this.decompress(path), "text/xml");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getSpine() {
|
|
|
|
return Array.from(this.opfXml.getElementsByTagName("spine")[0].children).map(node => node.getAttribute("idref"));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
resolves an idref in content.opf to its file
|
|
|
|
*/
|
|
|
|
resolveIDref(idref) {
|
2023-05-18 20:37:09 +00:00
|
|
|
return this.absPath(this.opfXml.getElementById(idref).getAttribute("href"));
|
2023-05-15 12:09:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* returns absolute path from path relative to content.opf
|
|
|
|
* @param path
|
|
|
|
*/
|
|
|
|
absPath(path) {
|
|
|
|
if (this.opfDir) {
|
|
|
|
return [this.opfDir, path].join("/");
|
|
|
|
} else {
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
returns the sum of the bytesize of all html files that are located before it in the spine
|
|
|
|
@param {string} currentFile idref of the current file, also part of the CFI, e.g. here: #epubcfi(/6/2[titlepage]!/4/1:0) it would be "titlepage"
|
|
|
|
*/
|
|
|
|
getPreviousFilesSize(currentFile) {
|
|
|
|
let bytesize = 0;
|
|
|
|
for (let file of this.getSpine()) {
|
|
|
|
if (file !== currentFile) {
|
2023-05-18 20:37:09 +00:00
|
|
|
let filepath = this.resolveIDref(file);
|
2023-05-15 12:09:18 +00:00
|
|
|
//ignore non text files
|
|
|
|
if (filepath.endsWith("html")) {
|
2023-05-21 20:23:53 +00:00
|
|
|
// console.log(filepath + " " + bytesize)
|
2023-05-15 12:09:18 +00:00
|
|
|
bytesize += this.files[filepath]._data.uncompressedSize;
|
|
|
|
}
|
|
|
|
} else {
|
2023-05-15 15:10:07 +00:00
|
|
|
break;
|
2023-05-15 12:09:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return bytesize;
|
|
|
|
}
|
2023-05-18 20:37:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* resolves the given cfi to the xml node it points to
|
|
|
|
* @param {string} cfi epub-cfi string in the form: epubcfi(/6/16[id13]!/4[id2]/4/2[doc12]/1:0)
|
|
|
|
* @return XML Text-Node
|
|
|
|
*/
|
|
|
|
cfiToXmlNode(cfi) {
|
|
|
|
let cfiPath = cfi.split("(")[1].split(")")[0];
|
|
|
|
let fileId = cfiPath.split("!")[0].split("[")[1].split("]")[0];
|
|
|
|
let xml = this.parser.parseFromString(this.decompress(this.resolveIDref(fileId)), "text/xml");
|
|
|
|
let components = cfiPath.split("!")[1].split("/").slice(1);
|
|
|
|
let currentNode = xml.getElementsByTagName("html")[0];
|
|
|
|
for (const component of components) {
|
|
|
|
this.validateChildNodes(currentNode);
|
2023-05-21 20:23:53 +00:00
|
|
|
// console.log(currentNode);
|
|
|
|
// console.log(component);
|
2023-05-18 20:37:09 +00:00
|
|
|
let index = 0;
|
|
|
|
if (component.includes("[")) {
|
|
|
|
index = parseInt(component.split("[")[0]) - 1;
|
|
|
|
currentNode = currentNode.childNodes[index];
|
|
|
|
console.assert(currentNode.getAttribute("id") === component.split("[")[1].split("]")[0], "failed to resolve node");
|
|
|
|
} else if (component.includes(":")) {
|
|
|
|
index = component.split(":")[0] - 1;
|
|
|
|
return currentNode.childNodes[index]; //exit point
|
|
|
|
} else {
|
|
|
|
index = parseInt(component);
|
|
|
|
currentNode = currentNode.childNodes[index - 1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* inserts missing text/element nodes to keep them alternating
|
|
|
|
* @param {*} parentNode
|
|
|
|
*/
|
|
|
|
validateChildNodes(parentNode) {
|
|
|
|
for (let index = 0; index < parentNode.childNodes.length;) {
|
|
|
|
const element = parentNode.childNodes[index];
|
|
|
|
if (index % 2 === 0 && element.nodeType === 1) {
|
|
|
|
element.parentNode.insertBefore(parentNode.ownerDocument.createTextNode(""), element);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (index % 2 === 1 && element.nodeType === 3) {
|
|
|
|
element.insertBefore(parentNode.ownerDocument.createElement("")); //TODO check
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
index++;
|
|
|
|
}
|
2023-05-15 12:09:18 +00:00
|
|
|
|
2023-05-15 15:10:07 +00:00
|
|
|
}
|
2023-05-18 20:37:09 +00:00
|
|
|
|
2023-05-15 15:10:07 +00:00
|
|
|
/**
|
2023-05-18 20:37:09 +00:00
|
|
|
takes the node that the cfi points at and counts the bytes of all nodes before that
|
2023-05-15 15:10:07 +00:00
|
|
|
*/
|
2023-05-18 20:37:09 +00:00
|
|
|
getCurrentFileProgress(CFI) {
|
|
|
|
let size = parseInt(CFI.split(":")[1])//text offset in node
|
|
|
|
let startnode = this.cfiToXmlNode(CFI); //returns text node
|
|
|
|
let xmlnsLength = startnode.parentNode.namespaceURI.length;
|
|
|
|
let prev = startnode.parentNode.previousElementSibling;
|
2023-05-15 15:10:07 +00:00
|
|
|
while (prev !== null) {
|
2023-05-21 20:23:53 +00:00
|
|
|
// console.log("size: "+size)
|
|
|
|
// console.log(prev.outerHTML)
|
|
|
|
// console.log(this.encoder.encode(prev.outerHTML).length - xmlnsLength)
|
2023-05-18 20:37:09 +00:00
|
|
|
size += this.encoder.encode(prev.outerHTML).length - xmlnsLength;
|
2023-05-15 15:10:07 +00:00
|
|
|
prev = prev.previousElementSibling;
|
|
|
|
}
|
2023-05-18 20:37:09 +00:00
|
|
|
let parent = startnode.parentElement.parentElement;
|
2023-05-15 15:10:07 +00:00
|
|
|
while (parent !== null) {
|
|
|
|
let parentPrev = parent.previousElementSibling;
|
|
|
|
while (parentPrev !== null) {
|
2023-05-21 20:23:53 +00:00
|
|
|
// console.log(parentPrev.outerHTML)
|
|
|
|
// console.log(this.encoder.encode(parentPrev.outerHTML).length - xmlnsLength)
|
2023-05-18 20:37:09 +00:00
|
|
|
|
|
|
|
size += this.encoder.encode(parentPrev.outerHTML).length - xmlnsLength;
|
2023-05-15 15:10:07 +00:00
|
|
|
parentPrev = parentPrev.previousElementSibling;
|
|
|
|
}
|
2023-05-18 20:37:09 +00:00
|
|
|
parent = parent.parentElement;
|
2023-05-15 15:10:07 +00:00
|
|
|
}
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
getProgress(currentFile, CFI) {
|
2023-05-21 20:23:53 +00:00
|
|
|
let percentage = (this.getPreviousFilesSize(currentFile) + this.getCurrentFileProgress(CFI))/this.getTotalByteLength();
|
2023-05-15 15:10:07 +00:00
|
|
|
if (percentage === Infinity) {
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
return percentage;
|
|
|
|
}
|
|
|
|
}
|
2023-05-15 12:09:18 +00:00
|
|
|
}
|
2023-05-21 20:23:53 +00:00
|
|
|
function waitFor(variable, callback) {
|
2023-05-22 21:36:14 +00:00
|
|
|
const interval = setInterval(function() {
|
2023-05-21 20:23:53 +00:00
|
|
|
if (variable!==undefined) {
|
|
|
|
clearInterval(interval);
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}, 200);
|
|
|
|
}
|
2023-05-18 20:37:09 +00:00
|
|
|
|
2023-05-21 20:23:53 +00:00
|
|
|
/**
|
|
|
|
* returns progress percentage
|
|
|
|
* @return {number}
|
|
|
|
*/
|
|
|
|
function calculateProgress(){
|
|
|
|
let data=reader.rendition.currentLocation().end;
|
2023-05-22 21:36:14 +00:00
|
|
|
return Math.round(epubParser.getProgress(epubParser.absPath(data.href),data.cfi)*100);
|
2023-05-21 20:23:53 +00:00
|
|
|
}
|
|
|
|
var epubParser;
|
|
|
|
waitFor(reader.book,()=>{
|
|
|
|
epubParser = new EpubParser(reader.book.archive.zip.files);
|
2023-05-22 21:36:14 +00:00
|
|
|
});
|
2023-05-21 20:23:53 +00:00
|
|
|
/*
|
2023-05-22 14:21:27 +00:00
|
|
|
register new event emitter locationchange that fires on urlchange
|
|
|
|
source: https://stackoverflow.com/a/52809105/21941129
|
2023-05-21 20:23:53 +00:00
|
|
|
*/
|
2023-05-22 14:21:27 +00:00
|
|
|
(() => {
|
|
|
|
let oldPushState = history.pushState;
|
|
|
|
history.pushState = function pushState() {
|
|
|
|
let ret = oldPushState.apply(this, arguments);
|
|
|
|
window.dispatchEvent(new Event('locationchange'));
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
let oldReplaceState = history.replaceState;
|
|
|
|
history.replaceState = function replaceState() {
|
|
|
|
let ret = oldReplaceState.apply(this, arguments);
|
|
|
|
window.dispatchEvent(new Event('locationchange'));
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('popstate', () => {
|
|
|
|
window.dispatchEvent(new Event('locationchange'));
|
|
|
|
});
|
|
|
|
})();
|
2023-05-22 21:36:14 +00:00
|
|
|
let progressDiv=document.getElementById("progress");
|
2023-05-22 14:21:27 +00:00
|
|
|
window.addEventListener('locationchange',()=>{
|
2023-05-22 21:36:14 +00:00
|
|
|
let newPos=calculateProgress();
|
|
|
|
console.log(newPos);
|
|
|
|
progressDiv.textContent=newPos+"%";
|
2023-05-22 14:21:27 +00:00
|
|
|
//getelement set element value
|
|
|
|
});
|