diff --git a/config.xml b/config.xml index 0884ffc..50a3364 100644 --- a/config.xml +++ b/config.xml @@ -19,6 +19,7 @@ + @@ -100,5 +101,6 @@ + diff --git a/package-lock.json b/package-lock.json index 4c85ace..00deb3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1239,14 +1239,6 @@ "@types/cordova": "^0.0.34" } }, - "@ionic-native/diagnostic": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@ionic-native/diagnostic/-/diagnostic-5.14.0.tgz", - "integrity": "sha512-hptAqkpfyORE3uJamItsBVBVdQwjU/4aO8JLtnueTadGSbPNQfYoXpYONZzz8/qOkrw+Uvqr6/e7Eu9Lsk0Iyg==", - "requires": { - "@types/cordova": "^0.0.34" - } - }, "@ionic-native/file": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-5.14.0.tgz", @@ -1422,9 +1414,9 @@ "dev": true }, "@types/node": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", - "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "version": "7.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.7.tgz", + "integrity": "sha512-4I7+hXKyq7e1deuzX9udu0hPIYqSSkdKXtjow6fMnQ3OR9qkxIErGHbGY08YrfZJrCS1ajK8lOuzd0k3n2WM4A==", "dev": true }, "@types/q": { @@ -3312,6 +3304,11 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.2.tgz", "integrity": "sha1-/Ajzci5n7ve2xnv8mag99q3Quro=" }, + "cordova-plugin-file": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.2.tgz", + "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA==" + }, "cordova-plugin-ionic-keyboard": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz", @@ -7433,6 +7430,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/node/-/node-12.10.0.tgz", + "integrity": "sha512-gzhu/wPSyupcu4TWzOoCsy9r1NONUZyzBQuItHeiRKWA5nov2pqHa+IO3CKNBHDu85nRw8yq13sw4I8S3aJxIQ==", + "requires": { + "node-bin-setup": "^1.0.0" + } + }, + "node-bin-setup": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.0.6.tgz", + "integrity": "sha512-uPIxXNis1CRbv1DwqAxkgBk5NFV3s7cMN/Gf556jSw6jBvV7ca4F9lRL/8cALcZecRibeqU+5dFYqFFmzv5a0Q==" + }, "node-fetch-npm": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", diff --git a/package.json b/package.json index 5e78ae0..1e82789 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@angular/platform-browser-dynamic": "~8.1.2", "@angular/router": "~8.1.2", "@ionic-native/core": "^5.0.0", - "@ionic-native/diagnostic": "^5.14.0", "@ionic-native/file": "^5.14.0", "@ionic-native/splash-screen": "^5.0.0", "@ionic-native/status-bar": "^5.0.0", @@ -29,6 +28,7 @@ "@ionic/angular": "^4.7.1", "cordova-android": "7.1.4", "cordova-plugin-device": "^2.0.2", + "cordova-plugin-file": "6.0.2", "cordova-plugin-ionic-keyboard": "^2.2.0", "cordova-plugin-ionic-webview": "^4.1.1", "cordova-plugin-splashscreen": "^5.0.2", @@ -37,6 +37,7 @@ "cordova-plugin-whitelist": "^1.3.3", "core-js": "^2.5.4", "epub": "^1.1.0", + "node": "^12.10.0", "rxjs": "~6.5.1", "tslib": "^1.9.0", "zone.js": "~0.9.1" @@ -53,7 +54,7 @@ "@ionic/angular-toolkit": "~2.0.0", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", - "@types/node": "~8.9.4", + "@types/node": "^7.10.7", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", @@ -78,10 +79,11 @@ "cordova-plugin-ionic-webview": { "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+" }, - "cordova-plugin-ionic-keyboard": {} + "cordova-plugin-ionic-keyboard": {}, + "cordova-plugin-file": {} }, "platforms": [ "android" ] } -} \ No newline at end of file +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b9a2d65..4d3aaee 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,7 +10,7 @@ import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { FilesManager } from './services/files.service'; import { File } from '@ionic-native/file/ngx'; -import { Diagnostic } from '@ionic-native/diagnostic/ngx'; +//import { Diagnostic } from '@ionic-native/diagnostic/ngx'; @NgModule({ declarations: [AppComponent], @@ -21,7 +21,6 @@ import { Diagnostic } from '@ionic-native/diagnostic/ngx'; SplashScreen, FilesManager, File, - Diagnostic, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } ], bootstrap: [AppComponent] diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index d96444e..f00c5f1 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -7,7 +7,7 @@ - + SD Card @@ -18,7 +18,11 @@ Internal - + + Recursive + + + + + + {{file}} + + diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 2333736..8dca30b 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -7,16 +7,18 @@ import { FilesManager } from '../services/files.service'; styleUrls: ['home.page.scss'], }) export class HomePage { - display: string + display: string; + which: number = 1; + recursive: boolean; constructor( private files: FilesManager ) { - files.listFileSys(1); + files.loadFiles(this.which, this.recursive); } - onScan(ev: any) { - console.log(ev); - this.files.listFileSys(parseInt(ev.detail.value)); + onScan() { + let which = typeof this.which == 'string' ? parseInt(this.which) : this.which; + this.files.loadFiles(which, this.recursive); } } diff --git a/src/app/services/epub.service.ts b/src/app/services/epub.service.ts new file mode 100644 index 0000000..d589a7e --- /dev/null +++ b/src/app/services/epub.service.ts @@ -0,0 +1,768 @@ +import { ZipFile } from "zipfile"; +import { xml2js } from 'xml2js'; +import { EventEmitter } from 'events'; + +export default class EPub extends EventEmitter { + version; + + filename; + imageroot; + linkroot; + zip; + xml2jsOptions; + + containerFile; + mimeFile; + rootFile: any; + + metadata:any = {}; + manifest = {}; + guide = []; + spine: any = {toc: false, contents: []}; + flow = []; + toc = []; + + constructor(fname, imageroot = null, linkroot = null) { + super(); + this.xml2jsOptions = xml2js.defaults['0.1']; + this.filename = fname; + + this.imageroot = (imageroot || "/images/").trim(); + this.linkroot = (linkroot || "/links/").trim(); + + if (this.imageroot.substr(-1) != "/") { + this.imageroot += "/"; + } + if (this.linkroot.substr(-1) != "/") { + this.linkroot += "/"; + } + } + + /** + * EPub#parse() -> undefined + * + * Starts the parser, needs to be called by the script + **/ + parse() { + this.containerFile = false; + this.mimeFile = false; + this.rootFile = false; + + this.metadata = {}; + this.manifest = {}; + this.guide = []; + this.spine = {toc: false, contents: []}; + this.flow = []; + this.toc = []; + + this.open(); + } + + + /** + * EPub#open() -> undefined + * + * Opens the epub file with Zip unpacker, retrieves file listing + * and runs mime type check + **/ + open() { + try { + this.zip = new ZipFile(this.filename); + } catch (E) { + this.emit("error", new Error("Invalid/missing file")); + return; + } + + if (!this.zip.names || !this.zip.names.length) { + this.emit("error", new Error("No files in archive")); + return; + } + + this.checkMimeType(); + }; + + /** + * EPub#checkMimeType() -> undefined + * + * Checks if there's a file called "mimetype" and that it's contents + * are "application/epub+zip". On success runs root file check. + **/ + checkMimeType() { + var i, len; + + for (i = 0, len = this.zip.names.length; i < len; i++) { + if (this.zip.names[i].toLowerCase() == "mimetype") { + this.mimeFile = this.zip.names[i]; + break; + } + } + if (!this.mimeFile) { + this.emit("error", new Error("No mimetype file in archive")); + return; + } + this.zip.readFile(this.mimeFile, (function (err, data) { + if (err) { + this.emit("error", new Error("Reading archive failed")); + return; + } + var txt = data.toString("utf-8").toLowerCase().trim(); + + if (txt != "application/epub+zip") { + this.emit("error", new Error("Unsupported mime type")); + return; + } + + this.getRootFiles(); + }).bind(this)); + }; + + /** + * EPub#getRootFiles() -> undefined + * + * Looks for a "meta-inf/container.xml" file and searches for a + * rootfile element with mime type "application/oebps-package+xml". + * On success calls the rootfile parser + **/ + getRootFiles() { + var i, len; + for (i = 0, len = this.zip.names.length; i < len; i++) { + if (this.zip.names[i].toLowerCase() == "meta-inf/container.xml") { + this.containerFile = this.zip.names[i]; + break; + } + } + if (!this.containerFile) { + this.emit("error", new Error("No container file in archive")); + return; + } + + this.zip.readFile(this.containerFile, (function (err, data) { + if (err) { + this.emit("error", new Error("Reading archive failed")); + return; + } + var xml = data.toString("utf-8").toLowerCase().trim(), + xmlparser = new xml2js.Parser(this.xml2jsOptions); + + xmlparser.on("end", (function (result) { + + if (!result.rootfiles || !result.rootfiles.rootfile) { + this.emit("error", new Error("No rootfiles found")); + console.dir(result); + return; + } + + var rootfile = result.rootfiles.rootfile, + filename = false, i, len; + + if (Array.isArray(rootfile)) { + + for (i = 0, len = rootfile.length; i < len; i++) { + if (rootfile[i]["@"]["media-type"] && + rootfile[i]["@"]["media-type"] == "application/oebps-package+xml" && + rootfile[i]["@"]["full-path"]) { + filename = rootfile[i]["@"]["full-path"].toLowerCase().trim(); + break; + } + } + + } else if (rootfile["@"]) { + if (rootfile["@"]["media-type"] != "application/oebps-package+xml" || !rootfile["@"]["full-path"]) { + this.emit("error", new Error("Rootfile in unknown format")); + return; + } + filename = rootfile["@"]["full-path"].toLowerCase().trim(); + } + + if (!filename) { + this.emit("error", new Error("Empty rootfile")); + return; + } + + + for (i = 0, len = this.zip.names.length; i < len; i++) { + if (this.zip.names[i].toLowerCase() == filename) { + this.rootFile = this.zip.names[i]; + break; + } + } + + if (!this.rootFile) { + this.emit("error", new Error("Rootfile not found from archive")); + return; + } + + this.handleRootFile(); + + }).bind(this)); + + xmlparser.on("error", (function (err) { + this.emit("error", new Error("Parsing container XML failed")); + return; + }).bind(this)); + + xmlparser.parseString(xml); + + + }).bind(this)); + }; + + /** + * EPub#handleRootFile() -> undefined + * + * Parses the rootfile XML and calls rootfile parser + **/ + handleRootFile() { + + this.zip.readFile(this.rootFile, (function (err, data) { + if (err) { + this.emit("error", new Error("Reading archive failed")); + return; + } + var xml = data.toString("utf-8"), + xmlparser = new xml2js.Parser(this.xml2jsOptions); + + xmlparser.on("end", this.parseRootFile.bind(this)); + + xmlparser.on("error", (function (err) { + this.emit("error", new Error("Parsing container XML failed")); + return; + }).bind(this)); + + xmlparser.parseString(xml); + + }).bind(this)); + }; + + /** + * EPub#parseRootFile() -> undefined + * + * Parses elements "metadata," "manifest," "spine" and TOC. + * Emits "end" if no TOC + **/ + parseRootFile(rootfile) { + + this.version = rootfile['@'].version || '2.0'; + + var i, len, keys, keyparts, key; + keys = Object.keys(rootfile); + for (i = 0, len = keys.length; i < len; i++) { + keyparts = keys[i].split(":"); + key = (keyparts.pop() || "").toLowerCase().trim(); + switch (key) { + case "metadata": + this.parseMetadata(rootfile[keys[i]]); + break; + case "manifest": + this.parseManifest(rootfile[keys[i]]); + break; + case "spine": + this.parseSpine(rootfile[keys[i]]); + break; + case "guide": + this.parseGuide(rootfile[keys[i]]); + break; + } + } + + if (this.spine.toc) { + this.parseTOC(); + } else { + this.emit("end"); + } + }; + + /** + * EPub#parseMetadata() -> undefined + * + * Parses "metadata" block (book metadata, title, author etc.) + **/ + parseMetadata(metadata) { + var i, j, len, keys, keyparts, key; + + keys = Object.keys(metadata); + for (i = 0, len = keys.length; i < len; i++) { + keyparts = keys[i].split(":"); + key = (keyparts.pop() || "").toLowerCase().trim(); + switch (key) { + case "publisher": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.publisher = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + } else { + this.metadata.publisher = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + } + break; + case "language": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.language = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").toLowerCase().trim(); + } else { + this.metadata.language = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").toLowerCase().trim(); + } + break; + case "title": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.title = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + } else { + this.metadata.title = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + } + break; + case "subject": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.subject = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + } else { + this.metadata.subject = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + } + break; + case "description": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.description = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + } else { + this.metadata.description = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + } + break; + case "creator": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.creator = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + this.metadata.creatorFileAs = String(metadata[keys[i]][0] && metadata[keys[i]][0]['@'] && metadata[keys[i]][0]['@']["opf:file-as"] || this.metadata.creator).trim(); + } else { + this.metadata.creator = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + this.metadata.creatorFileAs = String(metadata[keys[i]]['@'] && metadata[keys[i]]['@']["opf:file-as"] || this.metadata.creator).trim(); + } + break; + case "date": + if (Array.isArray(metadata[keys[i]])) { + this.metadata.date = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim(); + } else { + this.metadata.date = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim(); + } + break; + case "identifier": + if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"]["opf:scheme"] == "ISBN") { + this.metadata.ISBN = String(metadata[keys[i]]["#"] || "").trim(); + } else if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"].id && metadata[keys[i]]["@"].id.match(/uuid/i)) { + this.metadata.UUID = String(metadata[keys[i]]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim(); + } else if (Array.isArray(metadata[keys[i]])) { + for (j = 0; j < metadata[keys[i]].length; j++) { + if (metadata[keys[i]][j]["@"]) { + if (metadata[keys[i]][j]["@"]["opf:scheme"] == "ISBN") { + this.metadata.ISBN = String(metadata[keys[i]][j]["#"] || "").trim(); + } else if (metadata[keys[i]][j]["@"].id && metadata[keys[i]][j]["@"].id.match(/uuid/i)) { + this.metadata.UUID = String(metadata[keys[i]][j]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim(); + } + } + } + } + break; + } + } + + var metas = metadata['meta'] || {}; + Object.keys(metas).forEach(function(key) { + var meta = metas[key]; + if (meta['@'] && meta['@'].name) { + var name = meta['@'].name; + this.metadata[name] = meta['@'].content; + } + if (meta['#'] && meta['@'].property) { + this.metadata[meta['@'].property] = meta['#']; + } + + if(meta.name && meta.name =="cover"){ + this.metadata[meta.name] = meta.content; + } + }, this); + }; + + /** + * EPub#parseManifest() -> undefined + * + * Parses "manifest" block (all items included, html files, images, styles) + **/ + parseManifest(manifest) { + var i, len, path = this.rootFile.split("/"), element, path_str; + path.pop(); + path_str = path.join("/"); + + if (manifest.item) { + for (i = 0, len = manifest.item.length; i < len; i++) { + if (manifest.item[i]['@']) { + element = manifest.item[i]['@']; + + if (element.href && element.href.substr(0, path_str.length) != path_str) { + element.href = path.concat([element.href]).join("/"); + } + + this.manifest[manifest.item[i]['@'].id] = element; + + } + } + } + }; + + /** + * EPub#parseGuide() -> undefined + * + * Parses "guide" block (locations of the fundamental structural components of the publication) + **/ + parseGuide(guide) { + var i, len, path = this.rootFile.split("/"), element, path_str; + path.pop(); + path_str = path.join("/"); + + if (guide.reference) { + if(!Array.isArray(guide.reference)){ + guide.reference = [guide.reference]; + } + + for (i = 0, len = guide.reference.length; i < len; i++) { + if (guide.reference[i]['@']) { + + element = guide.reference[i]['@']; + + if (element.href && element.href.substr(0, path_str.length) != path_str) { + element.href = path.concat([element.href]).join("/"); + } + + this.guide.push(element); + + } + } + } + }; + + /** + * EPub#parseSpine() -> undefined + * + * Parses "spine" block (all html elements that are shown to the reader) + **/ + parseSpine(spine) { + var i, len, path = this.rootFile.split("/"), element; + path.pop(); + + if (spine['@'] && spine['@'].toc) { + this.spine.toc = this.manifest[spine['@'].toc] || false; + } + + if (spine.itemref) { + if(!Array.isArray(spine.itemref)){ + spine.itemref = [spine.itemref]; + } + for (i = 0, len = spine.itemref.length; i < len; i++) { + if (spine.itemref[i]['@']) { + if (element = this.manifest[spine.itemref[i]['@'].idref]) { + this.spine.contents.push(element); + } + } + } + } + this.flow = this.spine.contents; + }; + + /** + * EPub#parseTOC() -> undefined + * + * Parses ncx file for table of contents (title, html file) + **/ + parseTOC() { + var i, len, path = this.spine.toc.href.split("/"), id_list = {}, keys; + path.pop(); + + keys = Object.keys(this.manifest); + for (i = 0, len = keys.length; i < len; i++) { + id_list[this.manifest[keys[i]].href] = keys[i]; + } + + this.zip.readFile(this.spine.toc.href, (function (err, data) { + if (err) { + this.emit("error", new Error("Reading archive failed")); + return; + } + var xml = data.toString("utf-8"), + xmlparser = new xml2js.Parser(this.xml2jsOptions); + + xmlparser.on("end", (function (result) { + if (result.navMap && result.navMap.navPoint) { + this.toc = this.walkNavMap(result.navMap.navPoint, path, id_list); + } + + this.emit("end"); + }).bind(this)); + + xmlparser.on("error", (function (err) { + this.emit("error", new Error("Parsing container XML failed")); + return; + }).bind(this)); + + xmlparser.parseString(xml); + + }).bind(this)); + }; + + /** + * EPub#walkNavMap(branch, path, id_list,[, level]) -> Array + * - branch (Array | Object): NCX NavPoint object + * - path (Array): Base path + * - id_list (Object): map of file paths and id values + * - level (Number): deepness + * + * Walks the NavMap object through all levels and finds elements + * for TOC + **/ + walkNavMap(branch, path, id_list, level) { + level = level || 0; + + // don't go too far + if (level > 7) { + return []; + } + + var output = []; + + if (!Array.isArray(branch)) { + branch = [branch]; + } + + for (var i = 0; i < branch.length; i++) { + if (branch[i].navLabel) { + + var title = ''; + if (branch[i].navLabel && typeof branch[i].navLabel.text == 'string') { + title = branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel===branch[i].navLabel ? + (branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel || "").trim() : ''; + } + var order = Number(branch[i]["@"] && branch[i]["@"].playOrder || 0); + if (isNaN(order)) { + order = 0; + } + var href = ''; + if (branch[i].content && branch[i].content["@"] && typeof branch[i].content["@"].src == 'string') { + href = branch[i].content["@"].src.trim(); + } + + var element:any = { + level: level, + order: order, + title: title + }; + + if (href) { + href = path.concat([href]).join("/"); + element.href = href; + + if (id_list[element.href]) { + // link existing object + element = this.manifest[id_list[element.href]]; + element.title = title; + element.order = order; + element.level = level; + } else { + // use new one + element.href = href; + element.id = (branch[i]["@"] && branch[i]["@"].id || "").trim(); + } + + output.push(element); + } + } + if (branch[i].navPoint) { + output = output.concat(this.walkNavMap(branch[i].navPoint, path, id_list, level + 1)); + } + } + return output; + }; + + /** + * EPub#getChapter(id, callback) -> undefined + * - id (String): Manifest id value for a chapter + * - callback (Function): callback function + * + * Finds a chapter text for an id. Replaces image and link URL's, removes + * etc. elements. Return only chapters with mime type application/xhtml+xml + **/ + getChapter(id, callback) { + this.getChapterRaw(id, (function (err, str) { + if (err) { + callback(err); + return; + } + + var i, len, path = this.rootFile.split("/"), keys = Object.keys(this.manifest); + path.pop(); + + // remove linebreaks (no multi line matches in JS regex!) + str = str.replace(/\r?\n/g, "\u0000"); + + // keep only contents + str.replace(/]*?>(.*)<\/body[^>]*?>/i, function (o, d) { + str = d.trim(); + }); + + // remove