diff --git a/plugin/js/main.js b/plugin/js/main.js index 53cfee4b..09ee112e 100644 --- a/plugin/js/main.js +++ b/plugin/js/main.js @@ -619,6 +619,17 @@ var main = (function() { // actions to do when window opened window.onload = async () => { + if (typeof DOMPurify === "undefined" || typeof zip === "undefined") { + let msg = "Error: WebToEpub is missing required third-party dependencies (DOMPurify or zip.js).\n\nIf you are running from a git clone, please run 'npm install' in the project root to fetch these dependencies."; + alert(msg); + let pleaseWait = document.getElementById("findingChapterUrlsMessageRow"); + if (pleaseWait) { + pleaseWait.textContent = msg; + pleaseWait.style.color = "red"; + pleaseWait.hidden = false; + } + return; + } userPreferences = UserPreferences.readFromLocalStorage(); if (isRunningInTabMode()) { ErrorLog.SuppressErrorLog = false; diff --git a/plugin/js/parsers/FreeWebNovelParser.js b/plugin/js/parsers/FreeWebNovelParser.js index 4bfaeded..d302e51f 100644 --- a/plugin/js/parsers/FreeWebNovelParser.js +++ b/plugin/js/parsers/FreeWebNovelParser.js @@ -16,9 +16,55 @@ class FreeWebNovelParser extends Parser { this.minimumThrottle = 1000; } - async getChapterUrls(dom) { + async getChapterUrls(dom, chapterUrlsUI) { let menu = dom.querySelector("ul#idData"); - return util.hyperlinksToChapterList(menu); + let chapters = util.hyperlinksToChapterList(menu); + + let totalPage = 1; + let indexSelect = dom.querySelector("#indexselect"); + if (indexSelect) { + totalPage = indexSelect.querySelectorAll("option").length; + } else { + let scripts = [...dom.querySelectorAll("script")]; + for (let script of scripts) { + let match = /totalPage:\s*(\d+)/.exec(script.textContent); + if (match) { + totalPage = parseInt(match[1]); + break; + } + } + } + + if (totalPage > 1) { + chapterUrlsUI.showTocProgress(chapters); + let baseUrl = dom.baseURI; + let urlObj = new URL(baseUrl); + urlObj.search = ""; + urlObj.hash = ""; + let baseNovelUrl = urlObj.toString(); + + for (let page = 2; page <= totalPage; ++page) { + await this.rateLimitDelay(); + let url = `${baseNovelUrl}?ajax=chapters&page=${page}`; + try { + let response = await HttpClient.fetchJson(url); + if (response?.json?.code === 200 && response.json.html) { + let parser = new DOMParser(); + let tempDom = parser.parseFromString(response.json.html, "text/html"); + util.setBaseTag(url, tempDom); + let partialChapters = util.hyperlinksToChapterList(tempDom); + if (partialChapters.length > 0) { + chapterUrlsUI.showTocProgress(partialChapters); + chapters = chapters.concat(partialChapters); + } + } + } catch (e) { + console.error("Failed to fetch TOC page: " + page, e); + } + } + } + + return chapters; } extractTitleImpl(dom) { @@ -26,11 +72,16 @@ class FreeWebNovelParser extends Parser { } extractAuthor(dom) { - return dom.querySelector("[title=Author]").parentNode.querySelector("a").textContent; + let element = dom.querySelector("[title=Author]"); + return element ? element.parentNode.querySelector("a").textContent.trim() : ""; } extractSubject(dom) { - let tags = [...dom.querySelector("[title=Genre]").parentNode.querySelectorAll("a")]; + let element = dom.querySelector("[title=Genre]"); + if (!element) { + return ""; + } + let tags = [...element.parentNode.querySelectorAll("a")]; return tags.map(e => e.textContent.trim()).join(", "); } @@ -43,12 +94,77 @@ class FreeWebNovelParser extends Parser { } findContent(dom) { - return dom.querySelector("div.txt"); + return dom.querySelector("div#article") || dom.querySelector("div.txt"); } getInformationEpubItemChildNodes(dom) { return [...dom.querySelectorAll("div.inner")]; } + + removeUnwantedElementsFromContentElement(content) { + // Remove ads injected by third-party ad networks (such as SSP ads and PubFuture networks) + // whose div IDs start with 'bg-ssp-' or 'pf-' + util.removeChildElementsMatchingSelector(content, "div[id^='bg-ssp-'], div[id^='pf-']"); + + // Clean up any remaining ad divs or empty wrapper divs left behind after ads are deleted + for (let div of content.querySelectorAll("div")) { + if (div.id.startsWith("bg-ssp-") || div.id.startsWith("pf-")) { + div.remove(); + } + // Remove parent wrapper divs if they are now completely empty + if (div.children.length === 0 && div.textContent.trim() === "") { + div.remove(); + } + } + + // Convert escaped/literal HTML tags (like <strong> or <b>) in text nodes to actual DOM elements + let walker = content.ownerDocument.createTreeWalker( + content, + NodeFilter.SHOW_TEXT, + null, + false + ); + let nodesToReplace = []; + let node; + while ((node = walker.nextNode())) { + let val = node.nodeValue; + if (val && /( + @@ -145,6 +146,7 @@ +