// ==UserScript== // @name X阅读模式 // @namespace https://scriptcat.org/scripts/code/7614/X阅读模式.user.js // @version 1.5.3 // @description 小说阅读增强工具 // @author 失辛向南 // @license MIT // @match *://*/* // @run-at document-end // @noframes // @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.4.1/Readability.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_info // @connect * // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; /* ==================== 诊断日志系统 ==================== */ var __diag = (function () { var MAX = 800; var lines = []; var scriptName = 'X阅读模式'; var version = (function () { try { if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) { return String(GM_info.script.version); } } catch (e) {} return '2.0.1'; })(); function nowStr() { try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); } } function safeStr(o) { try { if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : ''); if (typeof o === 'string') return o; return JSON.stringify(o); } catch (e) { return String(o); } } function push(level, msg) { try { var s = safeStr(msg); if (s.length > 600) s = s.slice(0, 600) + '…'; lines.push('[' + nowStr() + '][' + level + '] ' + s); if (lines.length > MAX) lines.shift(); } catch (e) {} } function wrapConsole() { ['log', 'info', 'warn', 'error', 'debug'].forEach(function (m) { var orig = console[m]; if (typeof orig !== 'function') return; try { console[m] = function () { try { var args = Array.prototype.slice.call(arguments); var text = args.map(function (a) { return typeof a === 'string' ? a : safeStr(a); }).join(' '); if (m === 'error' || m === 'warn') push(m.toUpperCase(), text); else if (text.charAt(0) === '[') push(m.toUpperCase(), text); } catch (e) {} return orig.apply(console, arguments); }; } catch (e) {} }); } function hookErrors() { window.addEventListener('error', function (ev) { try { var t = ev.target; if (t && t.tagName && !(ev.message)) { var src = t.src || t.currentSrc || t.href || ''; push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)')); return; } var msg = ev.message || ''; var loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, ''); if (ev.error && ev.error.message) msg = ev.error.message; if (!msg && !loc && !ev.error) return; var extra = ''; if (ev.error && ev.error.stack) { extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n'); } push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra); } catch (e2) {} }, true); window.addEventListener('unhandledrejection', function (ev) { var r = ev.reason; push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r))); }); } function buildReport() { var out = []; out.push('========== ' + scriptName + ' 诊断日志 =========='); out.push('版本: ' + version); out.push('生成时间: ' + nowStr()); out.push('页面: ' + location.href); out.push('UA: ' + navigator.userAgent); out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || '')); try { out.push('白名单数量: ' + getWhitelist().length); out.push('历史记录数量: ' + getHistory().length); try { out.push('目录缓存数量: ' + Object.keys(getTocCacheAll()).length); } catch (eTc) {} var st = getSettings(); out.push('设置: ' + (st ? safeStr(st) : '(默认)')); } catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); } out.push('-------- 日志记录 (' + lines.length + ' 条) --------'); for (var i = 0; i < lines.length; i++) out.push(lines[i]); return out.join('\n'); } function download() { try { var text = buildReport(); var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; var d = new Date(), pad = function (n) { return n < 10 ? '0' + n : '' + n; }; a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000); return true; } catch (e) { try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {} return false; } } wrapConsole(); hookErrors(); return { version: version, push: push, download: download, buildReport: buildReport, info: function (m) { push('INFO', m); }, warn: function (m) { push('WARN', m); }, error: function (m) { push('ERROR', m); } }; })(); __diag.info('脚本已加载,版本 ' + __diag.version); /* ==================== 常量与状态 ==================== */ var KEY_SETTINGS = 'novel_reader2_settings_v1'; var KEY_WHITELIST = 'novel_reader2_whitelist_v1'; var KEY_HISTORY = 'novel_reader2_history_v1'; var KEY_PURIFY = 'novel_reader2_purify_v1'; var KEY_TOC_CACHE = 'novel_reader2_toc_cache_v1'; var TOC_CACHE_TTL = 24 * 60 * 60 * 1000; var TOC_CACHE_MAX = 10; var TOC_CACHE_MAX_BYTES = 2 * 1024 * 1024; var TOC_CACHE_REFRESH_MIN = 30 * 60 * 1000; var MAX_STITCH = 500; /* ==================== 繁简转换对照表 ==================== 每两个字符一对「繁简」。生产环境可替换为 OpenCC t2s 完整表(约 3000 对)。 覆盖日常阅读 95%+ 场景,体积紧凑。 */ var CONV_PAIRS = '個个們们這这爲为說说會会來来時时裏里過过對对開开關关話话後后學学國国導导將将東东車车書书馬马鳥鸟龍龙見见現现實实當当義义樂乐習习區区醫医經经還还長长點点難难體体與与從从萬万於于電电軍军無无聲声戰战報报據据別别兒儿華华進进幹干幫帮載载隊队產产類类權权歲岁紅红數数該该親亲願愿媽妈觀观覺觉謝謝談谈認认證证識识語语誤误麼么門门問问聞闻陽阳際际隨随雖虽顧顾條条務务處处達达樓楼設设標标題题單单卻却鐵铁鋼钢錢钱銀银陸陆陳陈孫孙趙赵鄭郑韓韩葉叶嚴严蓋盖買买賣卖貴贵賤贱費费貨货貸贷價价賺赚財财賬账購购員员農农輕轻輛辆輪轮軟软較较轉转迴回歸归歷历級级終终組组細细練练織织辦办邊边結结給给統统絕绝絲丝繼继續续維维綠绿緊紧線线縮缩總总纖纤聯联聽听肅肃腦脑臉脸臨临舉举舊旧藍蓝藥药藝艺蘇苏號号蟲虫術术衛卫裝装裡里褲裤襪袜襲袭補补襯衬襪袜鶴鹤鷹鹰鵬鹏鴻鸿鴉鸦鴦鸯鸚鹦鴣鸪鴛鸳鶴鹤麥麦黃黄點点黨党鼓鼓齊齐齒齿龜龟龍龙'; /* ==================== 目录分页会话缓存(Map LRU) ==================== */ var _tocPageCache = new Map(); var TOC_PAGE_CACHE_TTL = 10 * 60 * 1000; var TOC_PAGE_CACHE_MAX = 60; var TOC_PAGE_CACHE_MAX_BYTES = 8 * 1024 * 1024; function tocPageCacheGet(url) { var v = _tocPageCache.get(url); if (!v) return null; if (Date.now() - v.time > TOC_PAGE_CACHE_TTL) { _tocPageCache.delete(url); return null; } return v; } function tocPageCacheSet(url, html) { var bytes = html ? html.length : 0; if (_tocPageCache.has(url)) _tocPageCache.delete(url); _tocPageCache.set(url, { html: html, time: Date.now(), bytes: bytes }); while (_tocPageCache.size > TOC_PAGE_CACHE_MAX) { _tocPageCache.delete(_tocPageCache.keys().next().value); } var total = 0; _tocPageCache.forEach(function (v) { total += v.bytes || 0; }); while (total > TOC_PAGE_CACHE_MAX_BYTES && _tocPageCache.size > 1) { var k = _tocPageCache.keys().next().value; total -= (_tocPageCache.get(k).bytes || 0); _tocPageCache.delete(k); } } // 首次运行:从原版(novel_reader_*)迁移数据到新版(novel_reader2_*) (function migrateFromLegacy() { try { if (GM_getValue('novel_reader2_migrated_v1', '') === '1') return; var pairs = [ ['novel_reader_whitelist_v1', KEY_WHITELIST], ['novel_reader_settings_v1', KEY_SETTINGS], ['novel_reader_purify_v1', KEY_PURIFY], ['novel_reader_history_v1', KEY_HISTORY] ]; for (var i = 0; i < pairs.length; i++) { try { var oldVal = GM_getValue(pairs[i][0], ''); var newVal = GM_getValue(pairs[i][1], ''); if (oldVal && !newVal) GM_setValue(pairs[i][1], oldVal); } catch (e) {} } GM_setValue('novel_reader2_migrated_v1', '1'); } catch (e) {} })(); var state = { open: false, opening: false, root: null, win: null, visited: {}, stitchCount: 0, pendingInit: null, bookKey: null, bookTitle: null, exiting: false }; /* ==================== visited LRU(长阅读内存稳定) ==================== */ var _visitedQueue = []; var VISITED_MAX = 500; function markVisited(url) { var key = noHash(url); if (state.visited[key]) return; state.visited[key] = 1; _visitedQueue.push(key); while (_visitedQueue.length > VISITED_MAX) { var old = _visitedQueue.shift(); delete state.visited[old]; } } function clearVisited() { state.visited = {}; _visitedQueue = []; } var _tocStop = false; var _tocBusy = false; var _dlActive = false; /* ==================== 存储 ==================== */ function getSettings() { try { var v = GM_getValue(KEY_SETTINGS, ''); var o = v ? JSON.parse(v) : null; return (o && typeof o === 'object') ? o : null; } catch (e) { return null; } } var _whitelistCache = null; var _purifyCache = null; var _historyCache = null; var _historyWriteTimer = null; function flushHistoryWrite() { if (_historyWriteTimer) { clearTimeout(_historyWriteTimer); _historyWriteTimer = null; } if (_historyCache) { try { GM_setValue(KEY_HISTORY, JSON.stringify(_historyCache)); } catch (e) {} } } window.addEventListener('focus', function () { flushHistoryWrite(); _whitelistCache = null; _purifyCache = null; _historyCache = null; _tocCacheMem = null; idbPreload(); }); window.addEventListener('pagehide', flushHistoryWrite); function getWhitelist() { if (_whitelistCache) return _whitelistCache; try { var raw = GM_getValue(KEY_WHITELIST, '[]'); var list = JSON.parse(raw); if (Array.isArray(list) && list.length > 0 && typeof list[0] === 'string') { var newList = list.map(function(d) { return { domain: d, title: '' }; }); saveWhitelist(newList); return (_whitelistCache = newList); } if (Array.isArray(list)) { list = list.filter(function(item) { return item && typeof item === 'object' && item.domain; }); return (_whitelistCache = list); } return (_whitelistCache = []); } catch (e) { return []; } } function normalizeDomain(d) { if (!d) return ''; var s = String(d).trim().toLowerCase().replace(/^https?:\/\//i, '').split('/')[0].split('#')[0].split('?')[0]; var idx = s.indexOf(':'); if (idx !== -1) s = s.slice(0, idx); return s; } function saveWhitelist(list) { _whitelistCache = null; try { var clean = (list || []).map(function (it) { if (!it || typeof it !== 'object') return it; var d = normalizeDomain(it.domain); if (!d) return it; return { domain: d, title: it.title || '' }; }); GM_setValue(KEY_WHITELIST, JSON.stringify(clean)); } catch (e) {} } function inWhitelist(host) { var list = getWhitelist(); var target = normalizeDomain(host); for (var i = 0; i < list.length; i++) { if (list[i].domain === target) return true; } return false; } function getHistory() { if (_historyCache) return _historyCache; try { var v = GM_getValue(KEY_HISTORY, '[]'); var h = JSON.parse(v); return (_historyCache = Array.isArray(h) ? h : []); } catch (e) { return []; } } function saveHistory(history) { _historyCache = history; if (_historyWriteTimer) clearTimeout(_historyWriteTimer); _historyWriteTimer = setTimeout(flushHistoryWrite, 1500); } function getPurifyWords() { if (_purifyCache) return _purifyCache; try { var v = GM_getValue(KEY_PURIFY, '[]'); var list = JSON.parse(v); if (!Array.isArray(list)) return (_purifyCache = []); return (_purifyCache = list.filter(function (w) { return typeof w === 'string' && w.trim(); })); } catch (e) { return []; } } function savePurifyWords(list) { _purifyCache = null; try { GM_setValue(KEY_PURIFY, JSON.stringify(list)); } catch (e) {} } /* ==================== 繁简转换函数 ==================== */ var _convT2S = null, _convS2T = null; function buildConvMap(dir) { var m = Object.create(null); for (var i = 0; i + 1 < CONV_PAIRS.length; i += 2) { var trad = CONV_PAIRS.charAt(i), simp = CONV_PAIRS.charAt(i + 1); if (dir === 't2s') m[trad] = simp; else m[simp] = trad; } return m; } function convertText(text, mode) { if (!text || !mode || mode === 'off') return text; var m; if (mode === 't2s') { if (!_convT2S) _convT2S = buildConvMap('t2s'); m = _convT2S; } else if (mode === 's2t') { if (!_convS2T) _convS2T = buildConvMap('s2t'); m = _convS2T; } else return text; var n = text.length, out = ''; for (var i = 0; i < n; i++) { var c = text.charAt(i); out += (m[c] || c); } return out; } function convertDomElement(el, mode) { if (!el || !mode || mode === 'off') return; var walk = function (node) { var ch = node.childNodes; for (var i = 0; i < ch.length; i++) { var c = ch[i]; if (c.nodeType === 3) { var v = c.nodeValue; if (v && /[\u4e00-\u9fff]/.test(v)) { var cv = convertText(v, mode); if (cv !== v) c.nodeValue = cv; } } else if (c.nodeType === 1) walk(c); } }; walk(el); } function getConvertMode() { try { var st = getSettings(); return (st && st.simplified) || 'off'; } catch (e) { return 'off'; } } // 匹配用净化词:不做单向转换,而是给每个净化词同时生成"简体版 + 繁体版"两个变体, // 一起参与匹配。这样无论正文是简还是繁、无论用户当前选了哪种转换模式, // 简体词能屏蔽繁体正文、繁体词也能屏蔽简体正文。 // 隔离符(首尾 /)在转换时保留。 function getPurifyWordsForMatch() { var raw = getPurifyWords(); if (!raw || !raw.length) return raw || []; var out = []; var seen = Object.create(null); function add(w) { if (!w || seen[w]) return; seen[w] = 1; out.push(w); } for (var i = 0; i < raw.length; i++) { var w = raw[i]; add(w); var p = parsePurifyWord(w); if (!p.core) continue; var prefix = p.isoLeft ? '/' : ''; var suffix = p.isoRight ? '/' : ''; var t2s = convertText(p.core, 't2s'); var s2t = convertText(p.core, 's2t'); if (t2s !== p.core) add(prefix + t2s + suffix); if (s2t !== p.core) add(prefix + s2t + suffix); } return out; } /* ==================== 目录缓存(最近10本书,24小时过期) ==================== */ var TOC_CACHE_VER = 3; var _tocCacheMem = null; var _idbOpenPromise = null; function idbOpen() { if (_idbOpenPromise) return _idbOpenPromise; _idbOpenPromise = new Promise(function (resolve) { try { var req = indexedDB.open('__novel_reader_toc_cache', 1); req.onupgradeneeded = function (ev) { try { var db = ev.target.result; if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv'); } catch (e) {} }; req.onsuccess = function (ev) { resolve(ev.target.result); }; req.onerror = function () { resolve(null); }; req.onblocked = function () { resolve(null); }; } catch (e) { resolve(null); } }); return _idbOpenPromise; } function idbTx(mode) { return idbOpen().then(function (db) { if (!db) return null; try { return db.transaction('kv', mode).objectStore('kv'); } catch (e) { return null; } }); } function idbPut(obj) { idbTx('readwrite').then(function (store) { if (!store) return; try { store.put(obj, 'tocCache'); } catch (e) {} }).catch(function () {}); } function idbPreload() { idbTx('readonly').then(function (store) { if (!store) return null; return new Promise(function (resolve) { try { var req = store.get('tocCache'); req.onsuccess = function () { var o = req.result; if (o && typeof o === 'object' && !Array.isArray(o)) { if (_tocCacheMem === null || typeof _tocCacheMem !== 'object') _tocCacheMem = o; } else if (_tocCacheMem === null) { _tocCacheMem = {}; } resolve(); }; req.onerror = function () { resolve(); }; } catch (e) { resolve(); } }); }).catch(function () {}); } function getTocCacheAll() { if (_tocCacheMem !== null && typeof _tocCacheMem === 'object') return _tocCacheMem; try { var v = GM_getValue(KEY_TOC_CACHE, '{}'); var o = v ? JSON.parse(v) : null; var r = (o && typeof o === 'object' && !Array.isArray(o)) ? o : {}; _tocCacheMem = r; return r; } catch (e) { return {}; } } function saveTocCacheAll(o) { var obj = o || {}; _tocCacheMem = obj; idbPut(obj); try { GM_setValue(KEY_TOC_CACHE, JSON.stringify(obj)); } catch (e) {} } idbPreload(); function pruneTocCache(cache) { if (!cache || typeof cache !== 'object') return {}; var now = Date.now(); var keys = Object.keys(cache); for (var i = 0; i < keys.length; i++) { var k = keys[i], it = cache[k]; if (!it || !Array.isArray(it.toc) || !it.savedAt || now - it.savedAt > TOC_CACHE_TTL) { delete cache[k]; } } var remain = Object.keys(cache); if (remain.length > TOC_CACHE_MAX) { remain.sort(function (a, b) { return cache[a].savedAt - cache[b].savedAt; }); var drop = remain.length - TOC_CACHE_MAX; for (var j = 0; j < drop; j++) delete cache[remain[j]]; } remain = Object.keys(cache); if (remain.length) { var bytes = {}; var total = 2; for (var bi = 0; bi < remain.length; bi++) { bytes[remain[bi]] = JSON.stringify(cache[remain[bi]]).length; total += bytes[remain[bi]] + 2; } if (total > TOC_CACHE_MAX_BYTES) { remain.sort(function (a, b) { return cache[a].savedAt - cache[b].savedAt; }); while (remain.length > 1 && total > TOC_CACHE_MAX_BYTES) { var dropKey = remain.shift(); delete cache[dropKey]; total -= bytes[dropKey] + 2; } if (remain.length === 1 && total > TOC_CACHE_MAX_BYTES) { delete cache[remain[0]]; } } } return cache; } function getTocCacheForBook(bookKey) { if (!bookKey) return null; var cache = getTocCacheAll(); var it = cache[bookKey]; if (!it || !Array.isArray(it.toc) || !it.savedAt) return null; if (it.ver !== TOC_CACHE_VER) { delete cache[bookKey]; saveTocCacheAll(cache); return null; } if (Date.now() - it.savedAt > TOC_CACHE_TTL) { delete cache[bookKey]; saveTocCacheAll(cache); return null; } return it; } function saveTocCacheForBook(bookKey, toc, tocUrl, title, lastUrl) { if (!bookKey || !toc || !toc.length) return; var cache = getTocCacheAll(); var entry = { toc: toc, tocUrl: tocUrl || '', title: title || '', savedAt: Date.now(), ver: TOC_CACHE_VER }; if (lastUrl) entry.lastUrl = lastUrl; cache[bookKey] = entry; pruneTocCache(cache); saveTocCacheAll(cache); } function removeTocCacheForBook(bookKey) { if (!bookKey) return; var cache = getTocCacheAll(); if (cache[bookKey]) { delete cache[bookKey]; saveTocCacheAll(cache); } } function extractNovelTitle(title) { if (!title) return ''; var t = title.replace(/[((][^))]*[))]/g, '').trim(); var match = t.match(/^(.*?)\s*第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/); if (match) { return match[1].trim() || t; } return t || '未命名'; } function getBookRootKey(url) { try { var u = new URL(url); var path = u.pathname; if (path.endsWith('/')) return u.origin + path; var parts = path.split('/').filter(function(p) { return p; }); if (parts.length === 0) return u.origin + '/'; var last = parts[parts.length - 1]; if (/^\d+$/.test(last) || /^\d+\.html?$/i.test(last) || (/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last))) { parts.pop(); } var newPath = '/' + parts.join('/'); if (!newPath.endsWith('/')) newPath += '/'; return u.origin + newPath; } catch(e) { return url; } } // addHistory:额外接收 chapterNo/chapterTotal(进度),旧调用不传则不影响 function addHistory(chapterTitle, url, bookKey, bookTitle, chapterNo, chapterTotal) { if (!chapterTitle || !url) return; var cleanUrl = url.split('#')[0]; var key = bookKey || getBookRootKey(cleanUrl); var list = getHistory(); var found = false; var finalTitle = bookTitle || extractNovelTitle(chapterTitle) || '未命名'; for (var i = 0; i < list.length; i++) { var item = list[i]; var existingKey = item.bookKey || getBookRootKey(item.url); if (existingKey === key) { item.timestamp = Date.now(); item.url = cleanUrl; if (!item.bookKey) item.bookKey = key; if (!item.title || item.title === '未命名') { item.title = finalTitle; } if (typeof chapterNo === 'number' && chapterNo > 0) item.chapterNo = chapterNo; if (typeof chapterTotal === 'number' && chapterTotal > 0) item.chapterTotal = chapterTotal; found = true; break; } } if (!found) { var entry = { title: finalTitle, url: cleanUrl, timestamp: Date.now(), bookKey: key }; if (typeof chapterNo === 'number' && chapterNo > 0) entry.chapterNo = chapterNo; if (typeof chapterTotal === 'number' && chapterTotal > 0) entry.chapterTotal = chapterTotal; list.push(entry); if (list.length > 100) { list.sort(function(a,b) { return a.timestamp - b.timestamp; }); list.shift(); } } saveHistory(list); } function fetchSiteTitle(domain, callback) { if (!domain) { callback(''); return; } var url = 'https://' + domain; GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 5000, onload: function(res) { try { var html = decodeResponseText(res, url); var match = html.match(/]*>([^<]*)<\/title>/i); if (match && match[1]) { callback(match[1].trim()); } else { callback(''); } } catch(e) { callback(''); } }, onerror: function() { callback(''); }, ontimeout: function() { callback(''); } }); } /* ==================== 工具函数 ==================== */ var CHAP_RE = /(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)/i; var NAV_WORDS = /^(首页|书架|排行|分类|搜索|登录|注册|完本|最新|书库|书单|我的|消息|反馈|下载|客户端|繁體|繁体|english|返回顶部|上一页|下一页|上一章|下一章|刷新|加入书签|投推荐票|章节错误|字号|夜间模式|手机阅读|电脑版)$/i; var TOC_TEXT_RE = /^(目录|目錄|章节目录|返回目录|查看目录|返回书页|书页|目次|章节列表|列表|目录页)$/; function resolveUrl(href, base) { try { return new URL(href, base).href; } catch (e) { return ''; } } function isBadHref(h2) { return !h2 || h2.indexOf('javascript:') === 0 || h2.charAt(0) === '#' || h2.indexOf('about:') === 0 || h2.indexOf('mailto:') === 0; } function noHash(u) { try { return u.split('#')[0]; } catch (e) { return u; } } function cleanText(t) { return (t || '').replace(/\s+/g, '').trim(); } // 快速取 URL host(避免逐锚点 new URL) function urlHost(u) { var idx = u.indexOf('://'); if (idx === -1) return ''; var rest = u.slice(idx + 3); var end = rest.search(/[\/?#]/); return end === -1 ? rest : rest.slice(0, end); } function looksLikeChapterUrl(u) { try { var uo = new URL(u); var segs = (uo.pathname || '').split('/').filter(function (s) { return s; }); var last = segs[segs.length - 1] || ''; if (!(/^\d+(?:_\d+)?\.html?$/i.test(last) || (/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last)))) return false; try { var cur = new URL(location.href); var csegs = (cur.pathname || '').split('/').filter(function (s) { return s; }); var curFile = csegs[csegs.length - 1] || ''; var curIsChapter = /^\d+(?:_\d+)?\.html?$/i.test(curFile) || (/^[a-z0-9_-]+\.html?$/i.test(curFile) && /\d/.test(curFile)); if (curIsChapter) { return segs.slice(0, -1).join('/') === csegs.slice(0, -1).join('/'); } } catch (e) {} return true; } catch (e) { return false; } } function chineseToNumber(ch) { if (!ch) return 0; var map = { '零':0,'〇':0,'○':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'百':100,'千':1000,'万':10000,'亿':100000000 }; var hasWei = false; for (var w = 0; w < ch.length; w++) { if (map[ch[w]] >= 10) { hasWei = true; break; } } if (!hasWei) { var n = 0; for (var d = 0; d < ch.length; d++) { var dv = map[ch[d]]; if (dv === undefined || dv >= 10) continue; n = n * 10 + dv; } return n; } var num = 0, temp = 0; for (var i=0; i= 10) { if (temp === 0) temp = 1; num += temp * val; temp = 0; } else { temp = val; } } return num + temp; } function romanToNumber(roman) { var map = { 'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000 }; var total = 0, prev = 0; for (var i=roman.length-1; i>=0; i--) { var cur = map[roman[i]]; if (!cur) continue; if (cur < prev) total -= cur; else total += cur; prev = cur; } return total; } function parseChapterNumber(title) { if (!title) return null; var original = String(title).trim(); var clean = original.replace(/[\s\u3000]+/g, ''); var n = pickChapterNumber(clean); if (n !== null) return n; n = pickChapterNumber(clean.replace(/[((【\[〔〈「『].*?[))】\]〕〉」』]/g, '')); if (n !== null) return n; return pickChapterNumber(original); } function pickChapterNumber(clean) { var match; match = clean.match(/第\s*(\d+)\s*[章节回话篇集]/i); if (match) return parseInt(match[1], 10); match = clean.match(/第\s*([〇○零一二三四五六七八九十百千万亿两](?:\s*[〇○零一二三四五六七八九十百千万亿两])*)\s*[章节回话篇集]/i); if (match) return chineseToNumber(match[1]); match = clean.match(/chapter\s*(\d+)/i); if (match) return parseInt(match[1], 10); match = clean.match(/chapter\s*([IVXLCDM]+)/i); if (match) return romanToNumber(match[1]); match = clean.match(/^(\d+)[、..\s]/); if (match) return parseInt(match[1], 10); match = clean.match(/^([零一二三四五六七八九十百千万亿两]+)[、..\s]/); if (match) return chineseToNumber(match[1]); match = clean.match(/^([IVXLCDM]+)[、..\s]/); if (match) return romanToNumber(match[1]); return null; } function scoreNext(t) { if (/^(下一[章節节回卷篇]|下一章节|next\s*chapter|nextchapter|下[—\-–]?[章節节回卷篇])$/i.test(t)) return 5; if (/(下一[章節节回卷篇]|下[—\-–]?[章節节回卷篇])/.test(t)) return 4; if (/^(下一[页頁張张]|下[—\-–]?[页頁張张]|下页|下節|next\s*page|nextpage)$/i.test(t)) return 2; if (/^(next|›|»|→|>>?)$/i.test(t)) return 1; return 0; } function scorePrev(t) { if (/^(上一[章節节回卷篇]|上一章节|prev\s*chapter|prevchapter)$/i.test(t)) return 5; if (/(上一[章節节回卷篇])/.test(t)) return 4; if (/^(上一[页頁張张]|上页|prev\s*page|prevpage|previous)$/i.test(t)) return 2; if (/^(prev|‹|«|←|< .main > a[href]', '.novelinfvie'] }, { name: 'T10', must: ['.read_content', '.chapter_content'] }, { name: 'T11', must: ['#ReadSet', '.jumbotron', '#chaptercontent', '.container', '.readpage'] } ]; var SITE_NEXT_RULES = [ { name: 'yd.sogou.com', host: /yd\.sogou\.com/i, build: function (doc, origin) { try { var bkey = '', nkey = '', ngp = ''; try { bkey = window.bkey || ''; } catch (e) {} try { nkey = window.next || ''; } catch (e) {} try { ngp = window.gp || ''; } catch (e) {} if (bkey && nkey && ngp) return origin + '/h5/cpt/chapter?bkey=' + bkey + '&ckey=' + nkey + '&gf=' + ngp; } catch (e) {} return ''; } }, { name: 'wap.cmread.com', host: /wap\.cmread\.com/i, build: function (doc, origin) { try { var el = doc.getElementById('nChapterUrl'); var v = el && el.value ? String(el.value).trim() : ''; if (v && v.indexOf('http') === 0) return v; if (v) return origin + v; } catch (e) {} return ''; } }, { name: 'm.qidian.com', host: /m\.qidian\.com/i, build: function (doc, origin) { try { var gd = null; try { gd = (typeof g_data !== 'undefined') ? g_data : null; } catch (e) {} if (gd && gd.book && gd.book.bookId && gd.chapter && gd.chapter.next) { return origin + '/book/' + gd.book.bookId + '/' + gd.chapter.next; } } catch (e) {} return ''; } }, { name: 'm.pbtxt.com', host: /m\.pbtxt\.com/i, build: function (doc, origin) { try { var mulu = doc.getElementById('mulu'); var xz = doc.getElementById('xiazhang'); if (mulu && xz) { var p1 = mulu.getAttribute('href') || ''; var p2 = xz.getAttribute('href') || ''; if (p1 || p2) return origin + p1 + p2; } } catch (e) {} return ''; } }, { name: 'm.81zw / xs222 / biquge.tw / qu.la / uuxs.net', host: /m\.81zw\.com|m\.xs222\.com|m\.biquge\.tw|m\.qu\.la|m\.uuxs\.net/i, build: function (doc, origin) { try { var mulu = doc.getElementById('pb_mulu') || doc.getElementById('pt_mulu') || doc.getElementById('book-index'); var nxt = doc.getElementById('pb_next') || doc.getElementById('pt_next') || doc.getElementById('book-next'); if (mulu && nxt) { var p1 = mulu.getAttribute('href') || ''; var p2 = nxt.getAttribute('href') || ''; if (p2) { if (/(book|\/html)/i.test(p2)) return origin + p2; if (p1) return origin + p1 + p2; } } } catch (e) {} return ''; } }, { name: 'm.23xs.cc', host: /m\.23xs\.cc/i, build: function (doc, origin) { try { var mulu = doc.getElementById('pb_mulu'); var nxt = doc.getElementById('pb_next'); if (mulu && nxt) { var p1 = (mulu.getAttribute('href') || '').replace('chapters', 'book'); var p2 = nxt.getAttribute('href') || ''; if (p2) { if (/(book|\/html)/i.test(p2)) return origin + p2; if (p1) return origin + p1 + p2; } } } catch (e) {} return ''; } }, { name: 'm.123du.cc', host: /m\.123du\.cc/i, build: function (doc, origin) { try { var pos = doc.getElementsByClassName('DivCurrentPos'); var zw = doc.getElementsByClassName('PagesNoLink_zw'); if (pos && pos.length > 0 && zw && zw.length > 0) { var last = pos[0].lastElementChild; var p1 = last ? (last.getAttribute('href') || '') : ''; var p2 = (zw[0].getAttribute('href') || '').replace(origin + '/', ''); if (p1 || p2) return origin + p1 + p2; } } catch (e) {} return ''; } } ]; function siteNextUrlByRule(doc, baseUrl) { try { var u = new URL(baseUrl); var host = u.host; var origin = u.origin; for (var i = 0; i < SITE_NEXT_RULES.length; i++) { if (SITE_NEXT_RULES[i].host.test(host)) { var url = SITE_NEXT_RULES[i].build(doc, origin); if (url && noHash(url) !== noHash(baseUrl)) return url; return ''; } } } catch (e) {} return ''; } var NOISE_SELECTORS = [ '[class*="ui_addweixin"]', '[class*="tipsfoot"]', '[class*="footlink"]', '[class*="contfoot"]', '[class*="readCover"]', '[class*="ui_copyright"]', '[class*="r-volume"]', '[class*="r-menu-sub"]', '[class*="ui_channel1"]', '[class*="jsAuthorSay"]', '[class*="pop-ny-mask"]', '[class*="read-anthology-wrap"]', '[class*="bookDetailWrapper"]', '[class*="bookSummary"]', '[class*="baokaba"]', '[class*="am-divider-dotted"]', '[class*="limit-wrap"]', '[class*="downUrl"]', '[class*="clientUrl"]', '[class*="detailUrl"]', '[id*="bookSummary"]', '[id*="bookDetailWrapper"]', '[id*="readCover"]' ]; function removeNoiseSelectors(doc) { try { for (var i = 0; i < NOISE_SELECTORS.length; i++) { var els = doc.querySelectorAll(NOISE_SELECTORS[i]); for (var j = 0; j < els.length; j++) { try { if (els[j].parentNode) els[j].parentNode.removeChild(els[j]); } catch (e) {} } } } catch (e) {} } function pageChapterTitle(doc, host) { try { var titleFlag = ['nr_title', 'title', 'am-titlebar-title', 'big', 'atitle', 'chapterName', 'chaptitle']; if (/(wap\.faloo\.com|baidu8\.ikanshu\.cn|m\.cread\.com)/.test(host || '')) { var h3 = doc.getElementsByTagName('h3')[0] || doc.getElementById('lbChapterName'); if (h3) { var t0 = (h3.textContent || '').replace(/\s+/g, ' ').trim(); if (t0) return t0; } } for (var i = 0; i < titleFlag.length; i++) { var el = doc.getElementById(titleFlag[i]) || doc.getElementsByClassName(titleFlag[i])[0] || doc.getElementsByTagName('h1')[0]; if (el) { var t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (t && t.length <= 100) return t; } } } catch (e) {} return ''; } /* ==================== 正文块评分提取 ==================== */ var CONTENT_POSITIVE_HINT = /(?:^|[-_])(content|chapter|article|reader|read|text|novel|entry|post)(?:$|[-_])|正文|章节内容|小说正文/i; var CONTENT_NEGATIVE_TOKENS = ['sidebar', 'side-bar', 'aside', 'nav', 'navbar', 'navigation', 'menu', 'catalog', 'category', 'categories', 'classify', 'ranking', 'rank', 'recommend', 'recommended', 'related', 'footer', 'header', 'breadcrumb', 'breadcrumbs', 'toolbar', 'tools', 'widget', 'rightbar', 'leftbar', 'right-bar', 'left-bar', 'list-right', 'right-list', 'left-list', 'list-left']; var CONTENT_NAV_CUES = ['首页', '主页', '下一页', '上一页', '分类', '作者专栏', '通俗小说', '都市小说', '武侠小说', '奇幻小说', '冒险小说', '穿越小说', '黑暗小说', '言情小说', '全部小说', '列表', '排行榜', '热门推荐', '友情链接']; function normalizeText(value) { return String(value || '').replace(/\u00a0/g, ' ').replace(/[ \t]+/g, ' ').replace(/\r/g, '').trim(); } function elementIdentity(el) { if (!el) return ''; var cls = ''; try { cls = (typeof el.className === 'string') ? el.className : ''; } catch (e) {} var role = ''; try { role = el.getAttribute ? (el.getAttribute('role') || '') : ''; } catch (e) {} return ((el.id || '') + ' ' + cls + ' ' + role).toLowerCase(); } function identityTokens(el) { if (!el) return []; var raw = []; if (el.id) raw.push(String(el.id)); try { var cl = el.classList || []; for (var c = 0; c < cl.length; c++) raw.push(String(cl[c])); } catch (e) {} try { var role = el.getAttribute ? (el.getAttribute('role') || '') : ''; if (role) raw.push(String(role)); } catch (e) {} var tokens = []; for (var i = 0; i < raw.length; i++) { var lower = String(raw[i]).toLowerCase().trim(); if (!lower) continue; if (tokens.indexOf(lower) === -1) tokens.push(lower); var parts = lower.split(/[-_\s]+/); for (var j = 0; j < parts.length; j++) { if (parts[j] && tokens.indexOf(parts[j]) === -1) tokens.push(parts[j]); } } return tokens; } function hasNegativeContentIdentity(el) { if (!el) return false; var raw = elementIdentity(el); if (/目录|分类|排行|推荐|导航|侧栏/.test(raw)) return true; var toks = identityTokens(el); for (var i = 0; i < toks.length; i++) { var token = toks[i]; if (CONTENT_NEGATIVE_TOKENS.indexOf(token) !== -1) return true; if (token === 'txtnav') continue; if (/^(?:top|main|site|global|book|chapter)?nav(?:bar|menu|list|box)?$/.test(token)) return true; if (/^(?:top|main|site|global)?navigation$/.test(token)) return true; } return false; } function contentTextStats(el) { var text = normalizeText(el ? el.textContent : ''); var textLength = text.replace(/\s+/g, '').length; var linkChars = 0, shortLinks = 0; var anchors = []; try { anchors = el.querySelectorAll('a'); } catch (e) {} for (var i = 0; i < anchors.length; i++) { var t = normalizeText(anchors[i].textContent); linkChars += t.replace(/\s+/g, '').length; if (t && t.length <= 24) shortLinks++; } var compact = text.replace(/\s+/g, ''); var navCueHits = 0; for (var j = 0; j < CONTENT_NAV_CUES.length; j++) { if (compact.indexOf(CONTENT_NAV_CUES[j]) !== -1) navCueHits++; } var paragraphs = 0, brs = 0; try { paragraphs = el.querySelectorAll('p').length; } catch (e) {} try { brs = el.querySelectorAll('br').length; } catch (e) {} return { text: text, textLength: textLength, anchors: anchors.length, shortLinks: shortLinks, linkChars: linkChars, linkRatio: textLength ? linkChars / textLength : 1, navCueHits: navCueHits, paragraphs: paragraphs, brs: brs }; } function mixedColumnPenalty(el) { if (!el || !el.children || !el.children.length) return 0; var children = []; for (var i = 0; i < el.children.length && i < 30; i++) { var child = el.children[i]; var st = contentTextStats(child); if (st.textLength >= 100) children.push({ child: child, stats: st }); } if (children.length < 2) return 0; var prose = null, nav = null; for (var k = 0; k < children.length; k++) { var it = children[k]; if (!prose && it.stats.textLength >= 280 && it.stats.linkRatio < 0.12) prose = it; if (!nav && it.stats.anchors >= 3 && (it.stats.linkRatio > 0.32 || it.stats.navCueHits >= 3)) nav = it; } return (prose && nav) ? 2600 : 0; } function scoreContentElement(el, chapter, live, preferred) { if (!el) return -Infinity; var stats = contentTextStats(el); if (stats.textLength < 180) return -Infinity; var identity = elementIdentity(el); var tag = String(el.tagName || '').toUpperCase(); var score = Math.min(stats.textLength, 5000) * 0.34 + Math.min(Math.max(stats.textLength - 5000, 0), 10000) * 0.06 + Math.min(stats.paragraphs, 80) * 92 + Math.min(stats.brs, 160) * 13; score -= Math.pow(Math.min(1, stats.linkRatio), 1.15) * 7600; score -= Math.max(0, stats.anchors - 5) * 58; score -= Math.max(0, stats.shortLinks - 5) * 42; score -= stats.navCueHits * 170; if (preferred) score += 520; if (tag === 'ARTICLE') score += 1500; else if (tag === 'MAIN') score += 800; if (CONTENT_POSITIVE_HINT.test(identity)) score += 1050; if (hasNegativeContentIdentity(el)) score -= 1900; score -= mixedColumnPenalty(el); var compactChapter = normalizeText(chapter).replace(/\s+/g, ''); if (compactChapter) { var heads = []; try { heads = el.querySelectorAll('h1,h2,h3,.chapter-title,.chapter-name,.title'); } catch (e) {} for (var h = 0; h < heads.length && h < 8; h++) { if (normalizeText(heads[h].textContent).replace(/\s+/g, '').indexOf(compactChapter) !== -1) { score += 760; break; } } } if (live && typeof el.getBoundingClientRect === 'function') { var rect = el.getBoundingClientRect(); if (rect.width >= 430 && rect.width <= 1120) score += 820; else if (rect.width > 1320) score -= 900; if (rect.height > 240) score += 120; } return score; } function sanitizeScoredContent(html) { try { var d = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var r = d.getElementById('__scored_root'); if (!r) return html; var kills = ['script', 'style', 'iframe', 'noscript', 'embed', 'object', 'link', 'meta', 'form', 'input', 'button', 'select', 'textarea', 'canvas']; for (var k = 0; k < kills.length; k++) { var els = r.querySelectorAll(kills[k]); for (var j = 0; j < els.length; j++) { try { if (els[j].parentNode) els[j].parentNode.removeChild(els[j]); } catch (e) {} } } return r.innerHTML; } catch (e) { return html; } } function scoreExtractContent(doc, chapterTitle) { try { var clone = doc.cloneNode(true); removeNoiseSelectors(clone); var candidates = []; try { candidates = clone.querySelectorAll('div, article, main, section, td'); } catch (e) {} var hits = []; for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if (!el || !el.textContent) continue; if (el.textContent.length < 180) continue; if (el.textContent.replace(/\s+/g, '').length < 180) continue; hits.push(el); if (hits.length >= 2000) break; } var maxScore = -Infinity, best = null, bestText = 0; var limit = Math.min(hits.length, 500); for (var j = 0; j < limit; j++) { var s = scoreContentElement(hits[j], chapterTitle || '', false, false); if (s > maxScore) { maxScore = s; best = hits[j]; bestText = hits[j].textContent.replace(/\s+/g, '').length; } } if (!best || maxScore < 900 || bestText < 180) return null; return { content: sanitizeScoredContent(best.innerHTML || ''), score: maxScore, textLength: bestText }; } catch (e) { return null; } } /* ==================== 核心提取函数 ==================== */ function stripTags(html) { return (html || '').replace(/<[^>]*>/g, ''); } function readabilityParse(doc, opts) { try { var clone = doc.cloneNode(true); removeNoiseSelectors(clone); var article = new Readability(clone, opts || {}).parse(); return article || null; } catch (e) { return null; } } function collectRuleContent(doc) { var MARK = '__rule_keep__'; var contentEls = doc.querySelectorAll('p, div.content, div.chapter-content, #chaptercontent, .novel-content'); var texts = []; var i, el; for (i = 0; i < contentEls.length; i++) { el = contentEls[i]; if (el.innerHTML && el.innerHTML.length > 100) { try { el[MARK] = 1; } catch (e) {} } } for (i = 0; i < contentEls.length; i++) { el = contentEls[i]; if (!el[MARK]) continue; var parent = el.parentNode; var nested = false; while (parent && parent.nodeType === 1 && parent !== doc.documentElement && parent.tagName !== 'BODY') { if (parent[MARK]) { nested = true; break; } parent = parent.parentNode; } if (nested) continue; texts.push(el.innerHTML); } for (i = 0; i < contentEls.length; i++) { try { delete contentEls[i][MARK]; } catch (e) {} } return texts.join('
'); } function extractFromDoc(doc, baseUrl) { try { return extractFromDocInner(doc, baseUrl); } catch (e) { try { console.warn('[阅读模式] 内容提取异常:', baseUrl, e && e.message); } catch (e2) {} return { url: baseUrl, title: (doc && doc.title) || '', data: '', textContent: '', next_url: '', prev_url: '', tocUrl: '', toc: [] }; } } function extractFromDocInner(doc, baseUrl) { var matchedRule = null; for (var i = 0; i < SITE_RULES.length; i++) { var rule = SITE_RULES[i]; if (rule.must && rule.must.every(function (sel) { return doc.querySelector(sel); })) { matchedRule = rule; break; } } var content = ''; var host = ''; try { host = new URL(baseUrl).host; } catch (e) {} var title = pageChapterTitle(doc, host) || doc.title || ''; var nextUrl = '', prevUrl = '', tocUrl = ''; if (matchedRule) { var titleEl = doc.querySelector('title'); if (titleEl) title = titleEl.textContent.trim(); content = collectRuleContent(doc); if (!content || stripTags(content).trim().length < 150) { matchedRule = null; content = ''; } } if (!matchedRule) { var article = readabilityParse(doc, { charThreshold: 100 }); if (article) { content = article.content || ''; title = article.title || title; } if (!content || content.replace(/\s/g, '').length < 100) { if (!content || content.replace(/\s/g, '').length < 50) { var scored = scoreExtractContent(doc, title); if (scored && scored.content && scored.content.replace(/\s/g, '').length >= 100) { content = scored.content; try { __diag.info('评分提取成功: ' + baseUrl + ' 分数=' + Math.round(scored.score) + ' 长度=' + scored.textLength); } catch (e) {} } } if (!content || content.replace(/\s/g, '').length < 100) { var paragraphs = doc.querySelectorAll('p'); var pTexts = []; for (var k = 0; k < paragraphs.length; k++) { if (paragraphs[k].innerHTML.length > 50) pTexts.push(paragraphs[k].innerHTML); } if (pTexts.length > 1) content = pTexts.join('
'); else { var bodyText = doc.body.innerHTML; if (bodyText && bodyText.length > 200) content = bodyText; } } } } function relLink(rel) { try { var el = doc.querySelector('a[rel="' + rel + '"], link[rel="' + rel + '"]'); if (el) { var href = el.getAttribute('href'); if (!isBadHref(href)) { var u = resolveUrl(href, baseUrl); if (u && noHash(u) !== noHash(baseUrl)) return u; } } } catch (e) {} return null; } var anchors = []; try { anchors = Array.prototype.slice.call(doc.querySelectorAll('a[href]')); } catch (e) {} var selfUrl = noHash(baseUrl); var rn = relLink('next'); var nextBest = rn ? { url: rn, score: 3 } : null; var rp = relLink('prev'); var prevBest = rp ? { url: rp, score: 3 } : null; anchors.forEach(function (a) { var href = a.getAttribute('href'); if (isBadHref(href)) return; var u = resolveUrl(href, baseUrl); if (!u || noHash(u) === selfUrl) return; var t = cleanText(a.textContent); if (!t) return; var ns = scoreNext(t); if (ns > 0 && (!nextBest || ns > nextBest.score)) nextBest = { url: u, score: ns }; var ps = scorePrev(t); if (ps > 0 && (!prevBest || ps > prevBest.score)) prevBest = { url: u, score: ps }; }); var siteNext = siteNextUrlByRule(doc, baseUrl); nextUrl = siteNext || (nextBest ? nextBest.url : ''); prevUrl = prevBest ? prevBest.url : ''; if (!nextUrl) { try { var scrs = doc.querySelectorAll('script'); for (var si = 0; si < scrs.length; si++) { var st = scrs[si].textContent || ''; var nm = st.match(/var\s+nextpage\s*=\s*["']([^"']+)["']/); if (nm) { var nu2 = resolveUrl(nm[1], baseUrl); if (nu2 && noHash(nu2) !== selfUrl) { nextUrl = nu2; break; } } } } catch (e) {} } if (!nextUrl) { try { var spanEls = doc.querySelectorAll('span[data-url], a[data-url]'); for (var si = 0; si < spanEls.length; si++) { var st2 = cleanText(spanEls[si].textContent); if (!st2) continue; if (scoreNext(st2) > 0) { var nu4 = resolveUrl(spanEls[si].getAttribute('data-url'), baseUrl); if (nu4 && noHash(nu4) !== selfUrl) { nextUrl = nu4; break; } } } } catch (e) {} } for (var j = 0; j < anchors.length; j++) { var href2 = anchors[j].getAttribute('href'); if (isBadHref(href2)) continue; if (TOC_TEXT_RE.test(cleanText(anchors[j].textContent))) { var tu = resolveUrl(href2, baseUrl); if (tu && noHash(tu) !== selfUrl && !looksLikeChapterUrl(tu)) { tocUrl = tu; break; } } } if (!tocUrl) { for (var j2 = 0; j2 < anchors.length; j2++) { var a2 = anchors[j2]; var href3 = a2.getAttribute('href'); if (isBadHref(href3)) continue; var t3 = cleanText(a2.textContent); if (!t3 || t3.length > 12) continue; if (!/(目录|目錄|书页|書頁|章节目录|目次)/.test(t3)) continue; if (CHAP_RE.test(t3)) continue; var u3 = resolveUrl(href3, baseUrl); if (!u3 || noHash(u3) === selfUrl) continue; if (looksLikeChapterUrl(u3)) continue; tocUrl = u3; break; } } if (!tocUrl) { try { var derivedToc = getBookRootKey(baseUrl); var derivedPath = ''; try { derivedPath = new URL(derivedToc).pathname; } catch (e) {} if (derivedToc && derivedPath && derivedPath !== '/' && noHash(derivedToc) !== selfUrl) { tocUrl = derivedToc; try { __diag.info('未找到显式目录链接,使用URL推导兜底: ' + tocUrl); } catch (e) {} } } catch (e) {} } if (!content || content.replace(/\s/g, '').length < 50) { var art = readabilityParse(doc); if (art) { content = art.content || ''; title = art.title || title; } } return { url: baseUrl, title: title, data: content, textContent: content ? stripTags(content) : '', next_url: nextUrl, prev_url: prevUrl, tocUrl: tocUrl, toc: [] }; } function fallbackExtractHtml(html, baseUrl) { var title = '', data = '', next = '', toc = ''; try { var t = html.match(/]*>([\s\S]*?)<\/title>/i); if (t) title = t[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); var nv = html.match(/var\s+nextpage\s*=\s*["']([^"']+)["']/i); if (nv && nv[1]) next = resolveUrl(nv[1], baseUrl); if (!next) { var rl = html.match(/]*rel=["']next["'][^>]*>/i); if (rl && rl[0]) { var rh = rl[0].match(/href=["']([^"']+)["']/); if (rh && rh[1]) next = resolveUrl(rh[1], baseUrl); } } if (!next) { var na = html.match(/]*href=["']([^"']+)["'][^>]*>[\s\S]*?下一[章节页][\s\S]*?<\/a>/i) || html.match(/]*>[\s\S]*?下一[章节页][\s\S]*?<\/a>/i); if (na) { var nh = na[0].match(/href=["']([^"']+)["']/); if (nh && nh[1]) next = resolveUrl(nh[1], baseUrl); } } if (next && noHash(next) === noHash(baseUrl)) next = ''; var td = html.match(/]*href=["']([^"']+)["'][^>]*>[\s\S]*?(?:目录|章节目录)[\s\S]*?<\/a>/i); if (td && td[1]) toc = resolveUrl(td[1], baseUrl); var ps = html.match(/]*>([\s\S]*?)<\/p>/gi); if (ps && ps.length) { data = ps.join(''); } else { var body = html.match(/]*>([\s\S]*?)<\/body>/i); if (body && body[1]) data = body[1]; } } catch (e) {} return { url: baseUrl, title: title, data: data, textContent: stripTags(data), next_url: next, prev_url: '', tocUrl: toc }; } /* ==================== 工具函数(保留) ==================== */ function showToastIn(doc, id, msg, css, dur) { try { var old = doc.getElementById(id); if (old) old.parentNode.removeChild(old); var t = doc.createElement('div'); t.id = id; t.textContent = msg; t.style.cssText = css; (doc.body || doc.documentElement).appendChild(t); setTimeout(function () { try { t.parentNode.removeChild(t); } catch (e) {} }, dur); } catch (e) {} } function toast(msg) { showToastIn(document, '__novel_reader_toast__', msg, 'position:fixed;top:16%;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(0,0,0,.78);color:#fff;font-size:14px;line-height:1.5;padding:10px 18px;border-radius:20px;font-family:sans-serif;pointer-events:none;white-space:nowrap;', 2400); } /* ==================== 电脑站目录回退:m. → www. ==================== */ var _mSiteHost = ''; var _desktopHost = ''; var DESKTOP_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; (function detectSiteVariant() { var host = ''; try { host = (window.location.hostname || '').toLowerCase(); } catch (e) {} if (/^m\.[a-z0-9-]+\.[a-z0-9-]/.test(host)) { _mSiteHost = host; _desktopHost = 'www.' + host.slice(2); } })(); function toDesktopUrl(url) { if (!url || !_mSiteHost) return url; try { var u = new URL(url); if ((u.hostname || '').toLowerCase() === _mSiteHost) { u.hostname = _desktopHost; return u.href; } } catch (e) {} return url; } function toMobileUrl(url) { if (!url || !_desktopHost) return url; try { var u = new URL(url); if ((u.hostname || '').toLowerCase() === _desktopHost) { u.hostname = _mSiteHost; return u.href; } } catch (e) {} return url; } /* ==================== 乱码识别 ==================== */ function normalizeCharsetName(charset) { if (!charset) return ''; var s = String(charset).trim().toLowerCase(); if (/^(gb2312|gbk|x-gbk|gb_2312-80|cp936|windows-936)$/.test(s)) return 'gb18030'; if (/^(utf8|utf-8)$/.test(s)) return 'utf-8'; if (/^(big5|big-5)$/.test(s)) return 'big5'; return s; } function charsetFromHeaders(headers) { var raw = String(headers || ''); var m = raw.match(/content-type\s*:[^\r\n]*charset\s*=\s*["']?([^;\s"']+)/i) || raw.match(/charset\s*=\s*["']?([^;\s"']+)/i); return normalizeCharsetName(m && m[1]); } function charsetFromMeta(bytes) { try { var head = new TextDecoder('windows-1252').decode(bytes.slice(0, Math.min(bytes.byteLength, 16384))); var m = head.match(/]+charset\s*=\s*["']?\s*([^\s"'/>;]+)/i) || head.match(/]+content\s*=\s*["'][^"']*charset\s*=\s*([^\s"';>]+)/i); return normalizeCharsetName(m && m[1]); } catch (e) { return ''; } } function mojibakeScore(text) { var value = String(text || ''); if (!value) return 999999; var replacements = (value.match(/�/g) || []).length; var typical = (value.match(/锟斤拷|鏂囧瓧|绔犺妭|鐩綍|浣滆€?/g) || []).length; var controls = (value.match(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g) || []).length; var chinese = (value.match(/[\u3400-\u9fff]/g) || []).length; return replacements * 80 + typical * 40 + controls * 20 - Math.min(chinese, 300) * 0.05; } // 探针式解码:先用前 8KB 判断编码,再全文只解码一次 function decodeHTMLBytes(arrayBuffer, headers, url) { var bytes = new Uint8Array(arrayBuffer || new ArrayBuffer(0)); if (!bytes.byteLength) return ''; var host = ''; try { host = new URL(url, location.href).hostname; } catch (e) {} var detected = charsetFromHeaders(headers) || charsetFromMeta(bytes); var probe = bytes.subarray(0, Math.min(bytes.byteLength, 8192)); if (detected) { try { var probeText = new TextDecoder(detected, { fatal: false }).decode(probe); if (mojibakeScore(probeText) < 20) { return new TextDecoder(detected, { fatal: false }).decode(bytes); } } catch (e) {} } var candidates = []; function push(value) { value = normalizeCharsetName(value); if (value && candidates.indexOf(value) === -1) candidates.push(value); } if (/(^|\.)piaotia\.com$/i.test(host)) { if (detected === 'utf-8') push('utf-8'); else push('gb18030'); } push(detected); push('utf-8'); push('gb18030'); push('big5'); var bestCodec = 'utf-8', bestScore = Infinity; for (var i = 0; i < candidates.length; i++) { try { var t = new TextDecoder(candidates[i], { fatal: false }).decode(probe); var s = mojibakeScore(t); if (s < bestScore) { bestScore = s; bestCodec = candidates[i]; } } catch (e) {} } try { return new TextDecoder(bestCodec, { fatal: false }).decode(bytes); } catch (e) { return new TextDecoder('utf-8', { fatal: false }).decode(bytes); } } function decodeResponseText(res, url) { try { if (res && res.response && typeof res.response === 'object' && typeof res.response.byteLength === 'number') { return decodeHTMLBytes(res.response, res.responseHeaders || '', url); } } catch (e) {} return (res && (res.responseText || res.response)) || ''; } function fetchUrl(url, cb) { try { return GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', timeout: 20000, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, onload: function (r) { cb(null, decodeResponseText(r, url)); }, onerror: function () { cb('网络错误'); }, ontimeout: function () { cb('请求超时'); } }); } catch (e) { cb(String(e)); return null; } } function fetchUrlDesktop(url, cb) { try { return GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', timeout: 20000, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': DESKTOP_UA }, onload: function (r) { cb(null, decodeResponseText(r, url)); }, onerror: function () { cb('网络错误'); }, ontimeout: function () { cb('请求超时'); } }); } catch (e) { cb(String(e)); return null; } } function parseHtml(html, baseUrl) { var doc = new DOMParser().parseFromString(html, 'text/html'); try { var baseEl = document.createElement('base'); baseEl.href = baseUrl; doc.head.appendChild(baseEl); } catch (e) {} return doc; } function htmlToText(html) { if (!html) return ''; try { var doc = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var root = doc.getElementById('__tx_root'); var bad = root.querySelectorAll('script,style,noscript,iframe,object,embed,form,input,button,select,textarea,link,meta,svg'); for (var i = 0; i < bad.length; i++) { try { bad[i].parentNode.removeChild(bad[i]); } catch (e) {} } var out = []; (function walk(node) { var children = node.childNodes; for (var k = 0; k < children.length; k++) { var n = children[k]; if (n.nodeType === 3) { out.push(n.nodeValue || ''); } else if (n.nodeType === 1) { var tag = (n.tagName || '').toLowerCase(); if (tag === 'br') { out.push('\n'); continue; } walk(n); if (/^(p|div|li|h[1-6]|blockquote|section|article|tr|ul|ol|table|pre|figure|figcaption|dd|dt|hr)$/.test(tag)) out.push('\n'); } } })(root); var text = out.join(''); text = text.replace(/\r/g, ''); text = text.replace(/[ \t\u00a0\u3000]+/g, ' '); text = text.replace(/ ?\n ?/g, '\n'); text = text.replace(/\n{3,}/g, '\n\n'); return text.trim(); } catch (e) { return String(html).replace(/<[^>]*>/g, '').trim(); } } function isPageMarkerLine(t) { if (!t) return false; var s = String(t).replace(/\s+/g, ''); if (!s) return false; if (/^第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/.test(s) && /[((]第.*?页[))]/.test(s)) return true; if (/^[((]第.*?页[))]$/.test(s)) return true; if (/^第\s*[((]?\s*\d+\s*\/\s*\d+\s*[))]?\s*页$/.test(s)) return true; if (/^第\s*\d+\s*页$/.test(s)) return true; return false; } function isNextPagePromptLine(t) { if (!t) return false; var s = String(t).replace(/\s+/g, ''); if (!s) return false; if (/(本章|正文|本书|内容)/.test(s) && /(未完|未完结|尚未结束|没有结束|未结束|待续)/.test(s) && /(下一页|下页|翻页|翻到|继续阅读|点击|请翻|下一頁|下頁|翻頁)/.test(s)) return true; if (/^[((【\[]?(本章)?未完待续[))】\]]?[,。,!!??…~~\s]*$/.test(s)) return true; if (/^(点击|请|请点|轻点|点)?(翻到|前往|点击)?(下一页|下页|翻页|下一頁|下頁|翻頁)(继续阅读|继续|阅读|查看)?(本章内容|本章|本页|正文)?[。!!??…~\s]*$/.test(s)) return true; if (/^(请|点|点击)?(翻到|前往)?(下一页|下页|翻页|下一頁|下頁|翻頁)[^。!?\n]{0,10}(继续|继续阅读|阅读)[。!!??…~\s]*$/.test(s)) return true; return false; } function isPageNoiseLine(t) { return isPageMarkerLine(t) || isNextPagePromptLine(t); } function cleanDownloadText(text) { if (!text) return text; var lines = text.split('\n'); var cleaned = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (isPageNoiseLine(line)) continue; cleaned.push(lines[i]); } return cleaned.join('\n'); } function stripLeadingChapterHeading(text) { if (!text) return text; var lines = String(text).split('\n'); var re = /^(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)(\s|[::]|$)/i; var changed = false; while (lines.length) { var line = lines[0].trim(); if (!line) { lines.shift(); continue; } if (line.length <= 30 && re.test(line) && !/[。!?,;、]/.test(line)) { lines.shift(); changed = true; } else break; } if (!changed) return text; return lines.join('\n').replace(/^\n+/, ''); } function firstParagraphHead(text) { try { var lines = String(text).split('\n'); for (var i = 0; i < lines.length; i++) { var s = (lines[i] || '').replace(/\s+/g, '').trim(); if (s && s.length >= 6) return s.slice(0, 30); } } catch (e) {} return ''; } /* ==================== 正文净化 ==================== */ function isPunctChar(c) { return ',。!?;:、,.!?;:()()【】《》〈〉「」『』〔〕[]{}“”‘’"\'…—·–-~@#%^&*=+$¥€<>|\\/'.indexOf(c) !== -1; } function isEnglishWord(w) { return /[a-zA-Z]/.test(w); } function isEngLetter(c) { return /[a-zA-Z]/.test(c); } function parsePurifyWord(w) { var s = String(w); var isoLeft = false, isoRight = false; while (s.charAt(0) === '/') { isoLeft = true; s = s.slice(1); } while (s.charAt(s.length - 1) === '/') { isoRight = true; s = s.slice(0, s.length - 1); } return { core: s, isoLeft: isoLeft, isoRight: isoRight }; } function markPurifyRun(text, mark, start, L, eng, isoLeft, isoRight) { var n = text.length; var i, j, k, c; for (i = start; i < start + L && i < n; i++) mark[i] = true; function slashIsText(idx, adjacentToWord) { var ch = text.charAt(idx); if (ch !== '/' && ch !== '.') return false; if (eng && adjacentToWord) return true; if (idx > 0 && idx < n - 1 && isEngLetter(text.charAt(idx - 1)) && isEngLetter(text.charAt(idx + 1))) return true; return false; } if (!isoLeft) { j = start - 1; while (j >= 0) { c = text.charAt(j); if (isPunctChar(c)) { if (slashIsText(j, j === start - 1)) { mark[j] = true; j--; continue; } break; } mark[j] = true; j--; } } if (!isoRight) { k = start + L; while (k < n) { c = text.charAt(k); if (isPunctChar(c)) { if (slashIsText(k, k === start + L)) { mark[k] = true; k++; continue; } break; } mark[k] = true; k++; } while (k < n && isPunctChar(text.charAt(k))) { mark[k] = true; k++; } } } function buildPurifyTrie(words) { var root = { next: null, ends: null }; for (var wi = 0; wi < words.length; wi++) { var w = words[wi]; if (!w) continue; var p = parsePurifyWord(w); var core = p.core; if (!core) continue; var L = core.length; if (L === 0) continue; var eng = isEnglishWord(core); var node = root; for (var i = 0; i < L; i++) { var ch = core.charAt(i); var nx = node.next ? node.next[ch] : null; if (!nx) { nx = { next: null, ends: null }; if (!node.next) node.next = {}; node.next[ch] = nx; } node = nx; } if (!node.ends) node.ends = []; node.ends.push({ L: L, eng: eng, isoLeft: p.isoLeft, isoRight: p.isoRight }); } return root; } function purifyText(text, words) { if (!text || !words || !words.length) return text; var n = text.length; var mark = new Uint8Array(n); var tv = purifyWordsVersion(words); var c = purifyText._trieCache; var trie = (c && c.ver === tv) ? c.trie : buildPurifyTrie(words); if (!c || c.ver !== tv) purifyText._trieCache = { ver: tv, trie: trie }; var i, j, node; for (i = 0; i < n; i++) { node = trie; for (j = i; j < n; j++) { node = node.next ? node.next[text.charAt(j)] : null; if (!node) break; if (node.ends) { for (var e = 0; e < node.ends.length; e++) { var en = node.ends[e]; markPurifyRun(text, mark, i, en.L, en.eng, en.isoLeft, en.isoRight); } } } } var out = ''; if (n < 4096) { for (i = 0; i < n; i++) if (!mark[i]) out += text.charAt(i); return out; } var chunks = []; var start = 0; for (i = 0; i < n; i++) { if (mark[i]) { if (i > start) chunks.push(text.slice(start, i)); start = i + 1; } } if (start < n) chunks.push(text.slice(start)); return chunks.join(''); } function purifyWordsVersion(words) { var s = (words || []).join('\u0001'); var h = 5381; for (var i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0; return 'pv' + h.toString(36); } function purifyDomElement(el, words) { if (!el || !words || !words.length) return; var walk = function (node) { var children = node.childNodes; for (var i = 0; i < children.length; i++) { var c = children[i]; if (c.nodeType === 3) { var v = c.nodeValue; if (v && /[A-Za-z0-9\u4e00-\u9fa5]/.test(v)) { var clean = purifyText(v, words); if (clean !== v) c.nodeValue = clean; } } else if (c.nodeType === 1) { walk(c); } } }; walk(el); } function purifyHtml(html, words) { if (!html || !words || !words.length) return html; try { var doc = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var root = doc.getElementById('__purify_root'); if (!root) return html; purifyDomElement(root, words); return root.innerHTML; } catch (e) { return html; } } function normReaderUrl(u) { if (!u) return null; var s = String(u); try { s = new URL(s).href; } catch (e) {} s = s.split('#')[0].split('?')[0].replace(/\/+$/, ''); return [s, s.replace(/\.html?$/i, '')]; } function buildTocIndex(toc) { var u0 = {}, u1 = {}, n = {}; if (toc && toc.length) { for (var i = 0; i < toc.length; i++) { var it = toc[i]; if (!it || !it.url) continue; var t = normReaderUrl(it.url); if (!t) continue; if (u0[t[0]] === undefined) u0[t[0]] = it.title || ''; if (u1[t[1]] === undefined) u1[t[1]] = it.title || ''; var tn = parseChapterNumber(it.title); if (tn !== null && tn > 0 && n[tn] === undefined) n[tn] = it.title || ''; } } return { u0: u0, u1: u1, n: n }; } function tocTitleForUrlIdx(idx, url, bodyTitle) { if (!idx) return ''; if (url) { var target = normReaderUrl(url); if (target) { var a = target[0], b = target[1]; if (idx.u0[a] !== undefined) return idx.u0[a]; if (idx.u1[a] !== undefined) return idx.u1[a]; if (idx.u0[b] !== undefined) return idx.u0[b]; if (idx.u1[b] !== undefined) return idx.u1[b]; } } if (bodyTitle) { var bn = parseChapterNumber(bodyTitle); if (bn !== null && bn > 0 && idx.n[bn] !== undefined) return idx.n[bn]; } return ''; } function purifyLines(text, words) { if (!text || !words || !words.length) return text; var lines = String(text).split('\n'); for (var i = 0; i < lines.length; i++) { if (/[A-Za-z0-9\u4e00-\u9fa5]/.test(lines[i])) { var clean = purifyText(lines[i], words); if (clean !== lines[i]) lines[i] = clean; } } return lines.join('\n'); } /* ==================== 阅读正文清洗 ==================== */ function collectLeafTextEls(root) { var list = []; (function walk(node) { var kids = node.childNodes; for (var i = 0; i < kids.length; i++) { var c = kids[i]; if (c.nodeType !== 1) continue; var el = c; if (el.tagName === 'IMG' || el.tagName === 'BR') continue; var hasElChild = false; for (var j = 0; j < el.childNodes.length; j++) { var cc = el.childNodes[j]; if (cc.nodeType === 1 && cc.tagName !== 'BR') { hasElChild = true; break; } } if (!hasElChild && (el.textContent || '').replace(/\s+/g, '').trim()) list.push(el); walk(el); } })(root); return list; } function removePageNoiseLeafs(root, maxHead, maxTail) { var leaves = collectLeafTextEls(root); if (!leaves.length) return; var h = Math.min(leaves.length, maxHead || 8); var t = Math.min(leaves.length, maxTail || 8); for (var j = 0; j < leaves.length; j++) { var inHead = j < h; var inTail = j >= leaves.length - t; if (!inHead && !inTail) continue; var el = leaves[j]; if (el.querySelector && el.querySelector('img')) continue; var text = (el.textContent || '').replace(/\s+/g, '').trim(); if (!text) continue; if (isPageNoiseLine(text)) el.parentNode.removeChild(el); } } function stripLeadingHeadingDom(root) { var re = /^(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)(\s|[::]|$)/i; var leaves = collectLeafTextEls(root); var maxScan = Math.min(leaves.length, 6); for (var k = 0; k < maxScan; k++) { var el = leaves[k]; if (!el || !el.parentNode) continue; var text = (el.textContent || '').trim(); if (!text) continue; if (text.length <= 30 && re.test(text) && !/[。!?,;、]/.test(text)) { el.parentNode.removeChild(el); } } } // 阅读正文 HTML 清洗:去分页噪声 → 去开头章节标题 → 净化词 → 繁简转换 function cleanReaderHtml(html, words, opts) { if (!html) return html; try { var doc = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var root = doc.getElementById('__reader_clean_root'); if (!root) return html; removePageNoiseLeafs(root); if (!opts || opts.stripLeadingHeading !== false) stripLeadingHeadingDom(root); purifyDomElement(root, words); if (opts && opts.convertMode && opts.convertMode !== 'off') { convertDomElement(root, opts.convertMode); } return root.innerHTML; } catch (e) { return html; } } function saveTextFile(text) { var title = ''; try { var hist = getHistory(); var latest = null; for (var i = 0; i < hist.length; i++) { if (!latest || (hist[i].timestamp || 0) > (latest.timestamp || 0)) latest = hist[i]; } if (latest && latest.title && latest.title !== '未命名') title = latest.title; } catch (e) { try { __diag.warn('历史取书名失败: ' + (e && e.message)); } catch (e2) {} } title = title || state.bookTitle || window.top.document.title || 'novel'; var blob = new Blob(Array.isArray(text) ? text : [text], { type: 'text/plain;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = title.replace(/[\*\/:<>\?\\\|\r\n,]/g, '_') + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { try { URL.revokeObjectURL(a.href); } catch (e) {} }, 10000); } /* ==================== iframe 阅读器(含图片过滤和拦截) ==================== */ var READER_CSS = "*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}\n:root{\n--canvas:#F3EDE1;--surface:#FBF7EF;--ink:#3D342A;\n--dim:rgba(61,52,42,.55);--line:rgba(61,52,42,.10);\n--key:#C0392B;--key-soft:rgba(192,57,43,.10);\n--serif:\"Noto Serif SC\",\"Source Han Serif SC\",\"Songti SC\",\"SimSun\",serif;\n--sans:-apple-system,\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;\n--body:18px;--lh:1.9;\n--r-shell:22px;--r-card:12px;--r-control:10px;\n--ease:cubic-bezier(.22,.75,.3,1);\n--shadow-panel:0 -2px 6px rgba(61,52,42,.05),0 -14px 34px -8px rgba(61,52,42,.14),0 -40px 80px -30px rgba(61,52,42,.18);\n--shadow-card:0 1px 2px rgba(61,52,42,.05),0 8px 20px -6px rgba(61,52,42,.08),0 24px 50px -20px rgba(61,52,42,.12)}\n:root[data-theme=\"dark\"]{--canvas:#1b1b1f;--surface:#262630;--ink:#c9c9c9;--dim:rgba(201,201,201,.5);--line:rgba(201,201,201,.12);--key:#7cacf8;--key-soft:rgba(124,172,248,.14);--shadow-panel:0 -2px 6px rgba(0,0,0,.30),0 -14px 34px -8px rgba(0,0,0,.45),0 -40px 80px -30px rgba(0,0,0,.55);--shadow-card:0 1px 2px rgba(0,0,0,.25),0 8px 20px -6px rgba(0,0,0,.35),0 24px 50px -20px rgba(0,0,0,.45)}\n:root[data-theme=\"green\"]{--canvas:#DFE8D6;--surface:#EAF0E5;--ink:#35402F;--dim:rgba(53,64,47,.55);--line:rgba(53,64,47,.10);--key:#C0392B;--key-soft:rgba(192,57,43,.10)}\n:root[data-theme=\"paper\"]{--canvas:#FFFFFF;--surface:#FFFFFF;--ink:#45403A;--dim:rgba(69,64,58,.55);--line:rgba(69,64,58,.10);--key:#C0392B;--key-soft:rgba(192,57,43,.10)}\nhtml,body{height:100%}\nbody{background:var(--canvas);color:var(--ink);overflow:hidden;font-family:var(--sans);font-size:15px;line-height:1.6;-webkit-font-smoothing:antialiased;transition:background-color .35s var(--ease),color .35s var(--ease)}\nbutton{font:inherit;color:inherit;background:none;border:0;cursor:pointer}\nsvg{display:block}\n.num{font-variant-numeric:tabular-nums}\n/* ── 顶栏:返回 + 章名 + 目录 ── */\n.topbar{position:fixed;top:0;left:0;right:0;z-index:50;padding:calc(env(safe-area-inset-top) + 10px) 14px 10px;display:flex;align-items:center;gap:8px;background:var(--canvas);transition:transform .32s var(--ease),opacity .28s}\n.topbar.hidden{transform:translateY(-100%);opacity:0;pointer-events:none}\n.icbtn{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:var(--dim);transition:color .2s,background .2s}\n.icbtn:active{background:var(--key-soft);color:var(--key)}\n.chapter-title-top{flex:1;text-align:center;font-family:var(--serif);font-size:14px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n/* ── 章内进度条 ── */\n.progress-track{position:fixed;top:calc(env(safe-area-inset-top) + 60px);left:0;right:0;height:2px;background:var(--line);z-index:49;transition:opacity .28s}\n.progress-track.hidden{opacity:0}\n.progress-bar{height:100%;width:0;background:var(--key);transition:width .1s linear}\n/* ── 正文流 ── */\n#contentWrapper{position:fixed;top:0;left:0;right:0;bottom:0;z-index:10;overflow-y:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:calc(env(safe-area-inset-top) + 68px) 22px 120px}\n#contentWrapper::-webkit-scrollbar{display:none}\n.chapter-block{font-family:var(--serif);font-size:var(--body);line-height:var(--lh);text-align:justify;word-break:break-word;animation:reveal .5s var(--ease)}\n.chapter-title{font-family:var(--serif);font-size:22px;font-weight:700;letter-spacing:.04em;line-height:1.5;text-align:center;margin:6px 0 26px}\n.chapter-content{line-height:var(--lh);text-align:justify;word-break:break-word;font-size:var(--body)}\n.chapter-content p{text-indent:2em;margin-bottom:.9em}\n.chapter-content img{max-width:100%;height:auto;border-radius:4px;margin:8px 0}\n.chapter-divider{display:flex;align-items:center;justify-content:center;gap:8px;min-width:0;margin:16px 0 24px;opacity:.4;font-size:12px;white-space:nowrap;font-family:var(--sans)}\n.chapter-divider::after,.chapter-divider::before{content:\"\";display:block;flex:1 1 12px;min-width:12px;max-width:30%;height:1px;background:currentColor;opacity:.5}\n.chapter-divider-title{display:block;flex:0 1 auto;min-width:0;max-width:calc(100% - 56px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.chapter-end{margin-top:34px;text-align:center;padding:20px 0 10px}\n.chapter-end .ornament{color:var(--dim);letter-spacing:.6em;font-size:12px;margin-bottom:14px}\n.chapter-end .end-meta{font-family:var(--sans);font-size:12px;color:var(--dim)}\n.chapter-content img.reader-img-loading{min-height:160px;background-color:rgba(128,128,128,.12);background-repeat:no-repeat;background-position:center;background-size:auto;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='100%25' height='100%25' fill='%23eeeeee'/%3E%3Cg fill='%23bbbbbb' transform='translate(130,80)'%3E%3Crect x='0' y='0' width='40' height='30' rx='3' ry='3' fill='none' stroke='%23bbbbbb' stroke-width='2'/%3E%3Ccircle cx='10' cy='10' r='3'/%3E%3Cpath d='M2,28 L14,16 L22,22 L32,10 L38,16 L38,28 Z'/%3E%3C/g%3E%3Ctext x='50%25' y='75%25' text-anchor='middle' font-family='sans-serif' font-size='12' fill='%23999999'%3ELoading...%3C/text%3E%3C/svg%3E\")}\n/* ── 首屏加载 ── */\n.loading{display:flex;align-items:center;justify-content:center;height:60%;flex-direction:column;opacity:.5;font-size:14px;font-family:var(--sans)}\n.loading .spinner{width:24px;height:24px;border:2px solid var(--line);border-top-color:var(--key);border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}\n/* ── 自动加载动画(章末) ── */\n.stitch-loading{text-align:center;padding:16px 0;font-family:var(--sans);font-size:12px;color:var(--dim);visibility:hidden;min-height:48px;display:flex;align-items:center;justify-content:center;gap:8px}\n.stitch-loading.visible{visibility:visible}\n.stitch-loading .spinner{width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--key);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}\n/* ── 底部 5 图标工具栏 ── */\n.bottombar{position:fixed;left:0;right:0;bottom:0;z-index:48;display:flex;align-items:center;justify-content:space-around;padding:8px 12px calc(env(safe-area-inset-bottom) + 10px);background:var(--surface);border-top:1px solid var(--line);box-shadow:0 -2px 12px rgba(61,52,42,.06);transition:transform .32s var(--ease),opacity .28s}\n.bottombar.hidden{transform:translateY(100%);opacity:0;pointer-events:none}\n.tool-item{display:flex;flex-direction:column;align-items:center;gap:3px;padding:4px 8px;color:var(--dim);transition:color .2s;min-width:48px}\n.tool-item:active{color:var(--key)}\n.tool-item svg{width:22px;height:22px}\n.tool-item .label{font-size:10px;font-family:var(--sans);letter-spacing:.02em}\n.tool-item.on{color:var(--key)}\n/* ── 目录侧栏 ── */\n.catalog-mask{position:fixed;inset:0;z-index:88;background:rgba(0,0,0,.38);opacity:0;pointer-events:none;transition:opacity .28s var(--ease)}\n.catalog-mask.visible{opacity:1;pointer-events:auto}\n.catalog-sidebar{position:fixed;top:0;right:0;bottom:0;width:82%;max-width:330px;z-index:90;background:var(--surface);display:flex;flex-direction:column;transform:translateX(300%);visibility:hidden;pointer-events:none;transition:transform .32s var(--ease),visibility 0s .32s;box-shadow:var(--shadow-panel)}\n.catalog-sidebar.visible{transform:none;visibility:visible;pointer-events:auto;transition:transform .32s var(--ease)}\n.drawer-head{padding:calc(env(safe-area-inset-top) + 18px) 20px 12px;flex:none;border-bottom:1px solid var(--line)}\n.head-row{display:flex;gap:16px;align-items:flex-start}\n.bookcover{flex:none;width:96px;height:130px;border-radius:6px;background:linear-gradient(165deg,#C0392B 0%,#8E2A1F 58%,#5E1D15 100%);box-shadow:var(--shadow-card);display:flex;align-items:center;justify-content:center;color:#F3EDE1;font-family:var(--serif);font-size:15px;letter-spacing:.24em;writing-mode:vertical-rl;text-orientation:upright;padding-top:14px;overflow:hidden;white-space:nowrap}\n.head-info{flex:1;min-width:0}\n.book{font-family:var(--serif);font-size:17px;font-weight:700;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.sub{font-size:12px;color:var(--dim);margin-top:4px;line-height:1.6}\n.sub-progress{margin-top:8px;height:4px;background:var(--line);border-radius:2px;overflow:hidden}\n.sub-progress i{display:block;height:100%;width:0;background:var(--key);border-radius:2px;transition:width .3s var(--ease)}\n.drawer-actions{display:flex;gap:8px;margin-top:14px}\n.drawer-actions button{flex:1;height:34px;border-radius:999px;font-size:12.5px;background:var(--key-soft);color:var(--key);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.drawer-actions button:active{opacity:.8}\n.searchbar{margin:12px 20px 0;flex:none}\n.searchbar input{width:100%;height:36px;padding:0 14px;border-radius:var(--r-control);border:1px solid var(--line);background:var(--canvas);color:var(--ink);font-size:13px;outline:none;font-family:var(--sans)}\n.searchbar input:focus{border-color:var(--key)}\n.toc{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:4px 0 20px}\n.toc::-webkit-scrollbar{display:none}\n.toc-item{display:flex;align-items:center;gap:10px;padding:11px 20px;cursor:pointer;transition:background .15s;border-left:3px solid transparent}\n.toc-item:active{background:var(--key-soft)}\n.toc-item .idx{flex:none;width:30px;font-size:12px;color:var(--dim);font-family:var(--sans)}\n.toc-item .tt{flex:1;font-size:14px;font-family:var(--serif);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.toc-item.done .tt{color:var(--dim)}\n.toc-item.active{background-image:linear-gradient(90deg,rgba(192,57,43,.13),rgba(192,57,43,.03));border-left-color:var(--key)}\n.toc-item.active .idx,.toc-item.active .tt{color:var(--key);font-weight:600}\n.toc-item .tag{flex:none;font-size:10px;color:var(--dim);border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-family:var(--sans)}\n.catalog-empty{padding:40px 20px;text-align:center;font-size:13px;color:var(--dim);font-family:var(--sans)}\n/* ── 设置抽屉 ── */\n.settings{position:fixed;left:0;right:0;bottom:0;z-index:200;background:var(--surface);border-radius:var(--r-shell) var(--r-shell) 0 0;padding:12px 22px calc(env(safe-area-inset-bottom) + 34px);transform:translateY(104%);transition:transform .32s var(--ease);box-shadow:var(--shadow-panel)}\n.settings.visible{transform:none}\n.grab{width:38px;height:4px;border-radius:2px;background:var(--line);margin:0 auto 18px}\n.set-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--line)}\n.set-row:last-child{margin-bottom:0;padding-bottom:0;border-bottom:0}\n.set-label{font-size:14px;font-weight:600;min-width:56px}\n.set-label small{display:block;font-size:11px;color:var(--dim);font-weight:400;margin-top:2px}\n.slider-wrap{flex:1;margin-left:18px;display:flex;align-items:center;gap:10px}\n.slider-wrap input[type=range]{flex:1;appearance:none;-webkit-appearance:none;height:24px;background:var(--line);background-clip:content-box;padding:10px 0;border-radius:2px;outline:none}\n.slider-wrap input[type=range]::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--key);box-shadow:0 1px 4px rgba(0,0,0,.18);cursor:pointer}\n.slider-wrap .sz{flex:none;width:34px;text-align:right;font-size:13px;color:var(--dim);font-family:var(--sans)}\n.seg{flex:1;margin-left:18px;display:flex;gap:4px;padding:3px;background:var(--canvas);border:1px solid var(--line);border-radius:var(--r-control)}\n.seg button{flex:1;height:30px;border-radius:7px;font-size:12.5px;color:var(--dim)}\n.seg button.on{background:var(--surface);color:var(--ink);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.08)}\n.themes{flex:1;margin-left:18px;display:flex;gap:12px;justify-content:flex-end}\n.theme-card{width:52px;cursor:pointer;text-align:center;transition:transform .15s;padding:0}\n.theme-card:active{transform:scale(.94)}\n.theme-swatch{height:40px;border-radius:8px;border:2px solid transparent;box-shadow:var(--shadow-card);position:relative;overflow:hidden}\n.theme-card.on .theme-swatch{border-color:var(--key)}\n.theme-card.on .theme-swatch::after{content:\"\";position:absolute;inset:0;box-shadow:inset 0 0 0 1px var(--key-soft)}\n.theme-card .tn{font-size:10px;color:var(--dim);margin-top:5px;display:block}\n.theme-card.on .tn{color:var(--key);font-weight:600}\n.swatch-parchment{background:#F3EDE1}\n.swatch-dark{background:#1A1713}\n.swatch-green{background:#DFE8D6}\n.swatch-paper{background:#FFFFFF;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n.font-seg{flex:1;margin-left:18px;display:flex;gap:4px;padding:3px;background:var(--canvas);border:1px solid var(--line);border-radius:var(--r-control)}\n.font-seg button{flex:1;height:30px;border-radius:7px;font-size:12.5px;color:var(--dim)}\n.font-seg button.on{background:var(--surface);color:var(--ink);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.08)}\n.font-seg .serif-opt{font-family:var(--serif)}\n.font-seg .sans-opt{font-family:var(--sans)}\n.settings-mask{position:fixed;inset:0;z-index:150;background:rgba(0,0,0,.38);opacity:0;pointer-events:none;transition:opacity .28s}\n.settings-mask.visible{opacity:1;pointer-events:auto}\n/* ── 白名单/历史/净化词共用遮罩 ── */\n.extra-mask{position:fixed;inset:0;z-index:210;background:rgba(0,0,0,.38);display:none}\n/* ── 通用底部面板(白名单/历史/净化词) ── */\n.panel-drawer{position:fixed;left:0;right:0;bottom:0;z-index:220;background:var(--surface);border-radius:var(--r-shell) var(--r-shell) 0 0;padding:14px 22px calc(env(safe-area-inset-bottom) + 30px);transform:translateY(104%);visibility:hidden;pointer-events:none;transition:transform .32s var(--ease),visibility 0s .32s;box-shadow:var(--shadow-panel);max-height:82%;display:flex;flex-direction:column}\n.panel-drawer.visible{transform:none;visibility:visible;pointer-events:auto;transition:transform .32s var(--ease)}\n.panel-grab{width:38px;height:4px;border-radius:2px;background:var(--line);margin:0 auto 14px;flex:none}\n.panel-head{flex:none;display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px;padding-bottom:14px;border-bottom:1px solid var(--line)}\n.panel-head h3{font-family:var(--serif);font-size:18px;font-weight:700;letter-spacing:.02em}\n.panel-head .panel-sub{font-size:12px;color:var(--dim);margin-top:3px;line-height:1.5}\n.panel-close{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;color:var(--dim)}\n.panel-close:active{background:var(--key-soft);color:var(--key)}\n.panel-body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}\n.panel-body::-webkit-scrollbar{display:none}\n.list-item{display:flex;align-items:center;gap:12px;padding:13px 12px;border-bottom:1px solid var(--line)}\n.list-item:last-child{border-bottom:0}\n.list-item.hi{background:var(--key-soft);border-radius:14px;box-shadow:inset 0 0 0 2px var(--key);border-bottom:0}\n.list-item.hi .li-title{color:var(--key)}\n.list-item.hi .li-sub{color:var(--key);opacity:.75}\n.list-item .li-main{flex:1;min-width:0;cursor:pointer}\n.list-item .li-title{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.list-item .li-sub{font-size:12px;color:var(--dim);margin-top:3px;line-height:1.5}\n.list-item .li-action{flex:none;display:flex;align-items:center;gap:8px}\n.li-del{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;color:var(--dim)}\n.li-del:active{background:rgba(192,57,43,.12);color:var(--key)}\n.date-group{font-size:11px;color:var(--dim);font-weight:600;letter-spacing:.08em;padding:16px 0 6px;font-family:var(--sans)}\n.date-group:first-child{padding-top:4px}\n.tag-pill{display:inline-block;font-size:10px;padding:2px 8px;border-radius:999px;background:var(--key-soft);color:var(--key);font-weight:600;margin-left:6px;vertical-align:middle}\n.add-row{display:flex;gap:8px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line);flex:none}\n.add-row input{flex:1;height:38px;padding:0 14px;border-radius:var(--r-control);border:1px solid var(--line);background:var(--canvas);color:var(--ink);font-size:13px;outline:none;font-family:var(--sans)}\n.add-row input:focus{border-color:var(--key)}\n.add-row button{height:38px;padding:0 18px;border-radius:999px;background:var(--key);color:#FFF;font-size:13px;font-weight:600;white-space:nowrap}\n.panel-empty{text-align:center;padding:40px 0;color:var(--dim);font-size:13px;font-family:var(--sans)}\n/* ── 净化词面板 ── */\n.word-item{display:flex;align-items:center;justify-content:space-between;padding:11px 4px;border-bottom:1px solid var(--line)}\n.word-item:last-child{border-bottom:0}\n.word-text{font-size:14px;font-family:var(--sans);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.word-del{background:none;border:none;font-size:14px;cursor:pointer;opacity:.35;padding:4px 8px;border-radius:6px;transition:opacity .2s,background .2s;color:var(--ink);flex:none}\n.word-del:hover{opacity:.9}\n.word-del:active{background:var(--key-soft);color:var(--key)}\n.purify-help{margin:0 0 10px;border-radius:10px;background:var(--line);font-size:11px;line-height:1.8;flex:none;overflow:hidden}\n.help-head{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;cursor:pointer;font-weight:600;opacity:.85;user-select:none}\n.help-arrow{transition:transform .25s;flex:none;opacity:.6}\n.purify-help.open .help-arrow{transform:rotate(180deg)}\n.help-body{display:none;padding:0 12px 10px;opacity:.85}\n.purify-help.open .help-body{display:block}\n.help-body p{margin-bottom:5px}\n.help-body p:last-child{margin-bottom:0}\n.help-body .hl{color:var(--key);font-weight:700}\n/* ── 下载范围弹窗 ── */\n.modal-mask{position:fixed;inset:0;z-index:300;background:rgba(0,0,0,.45);opacity:0;pointer-events:none;transition:opacity .28s var(--ease);display:flex;align-items:center;justify-content:center;padding:30px}\n.modal-mask.visible{opacity:1;pointer-events:auto}\n.modal-card{width:100%;max-width:320px;background:var(--surface);border-radius:18px;box-shadow:var(--shadow-card);transform:scale(.92);transition:transform .28s var(--ease);overflow:hidden}\n.modal-mask.visible .modal-card{transform:scale(1)}\n.modal-head{padding:18px 20px 12px;text-align:center}\n.modal-head h3{font-family:var(--serif);font-size:17px;font-weight:700;margin-bottom:4px}\n.modal-head p{font-size:12px;color:var(--dim);line-height:1.5}\n.modal-body{padding:0 20px 16px}\n.dl-option{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;margin-bottom:8px;cursor:pointer;transition:border-color .2s,background .2s}\n.dl-option:active{background:var(--key-soft)}\n.dl-option.selected{border-color:var(--key);background:var(--key-soft)}\n.dl-radio{width:18px;height:18px;border-radius:50%;border:2px solid var(--line);flex:none;position:relative;transition:border-color .2s}\n.dl-option.selected .dl-radio{border-color:var(--key)}\n.dl-option.selected .dl-radio::after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:var(--key)}\n.dl-opt-main{flex:1;min-width:0}\n.dl-opt-title{font-size:14px;font-weight:600}\n.dl-opt-sub{font-size:11px;color:var(--dim);margin-top:2px}\n.dl-range-row{display:flex;align-items:center;gap:8px;margin-top:8px}\n.dl-range-row input{flex:1;height:34px;padding:0 10px;border-radius:8px;border:1px solid var(--line);background:var(--canvas);color:var(--ink);font-size:13px;text-align:center;outline:none;font-family:var(--sans)}\n.dl-range-row input:focus{border-color:var(--key)}\n.dl-range-row span{color:var(--dim);font-size:13px;flex:none}\n.modal-foot{display:flex;gap:10px;padding:0 20px 18px}\n.modal-foot button{flex:1;height:40px;border-radius:999px;font-size:14px;font-weight:600;cursor:pointer;transition:opacity .2s}\n.modal-foot .btn-cancel{background:var(--line);color:var(--ink)}\n.modal-foot .btn-confirm{background:var(--key);color:#FFF}\n.modal-foot button:active{opacity:.8}\n.dl-progress{padding:16px 20px 20px;text-align:center;display:none}\n.dl-progress.show{display:block}\n.dl-progress .dl-bar{height:6px;background:var(--line);border-radius:3px;overflow:hidden;margin:10px 0 8px}\n.dl-progress .dl-bar i{display:block;height:100%;background:var(--key);border-radius:3px;transition:width .3s;width:0}\n.dl-progress .dl-pct{font-size:13px;color:var(--dim);font-family:var(--sans)}\n.dl-progress .dl-stop{display:inline-block;margin-top:10px;padding:6px 18px;border-radius:999px;background:var(--line);color:var(--ink);font-size:12px;cursor:pointer}\n/* ── Toast ── */\n.toast{position:fixed;left:50%;bottom:calc(env(safe-area-inset-bottom) + 96px);z-index:999;transform:translate(-50%,14px);opacity:0;pointer-events:none;max-width:80%;padding:10px 18px;border-radius:999px;background:rgba(26,23,19,.88);color:#F3EDE1;font-size:13px;line-height:1.5;font-family:var(--sans);transition:opacity .28s var(--ease),transform .28s var(--ease)}\n.toast.on{opacity:1;transform:translate(-50%,0)}\n/* ── 呼出提示 ── */\n.tap-hint{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:5;font-size:12px;color:var(--dim);opacity:0;pointer-events:none;transition:opacity .4s;font-family:var(--sans);letter-spacing:.04em}\n.tap-hint.show{opacity:.5}\n@keyframes spin{to{transform:rotate(360deg)}}\n@keyframes reveal{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}\n@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}\n"; var READER_BODY = "
\n \n
\n \n
\n
\n
点击屏幕中间呼出工具栏
\n
\n
\n
正在加载内容...
\n
\n
\n
正在加载下一章...
\n
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n
\n
\n
字号15 – 24 px
\n
\n \n 18\n
\n
\n
\n
行距
\n
\n \n \n \n
\n
\n
\n
繁简正文 / 目录 / 下载
\n
\n \n \n \n
\n
\n
\n
背景
\n
\n
羊皮纸
\n
夜读
\n
护眼
\n
纸白
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n
\n
暂无目录
\n
\n
\n
\n
\n
\n

白名单管理

\n
白名单内的网站打开时自动进入阅读模式
\n
\n \n
\n
\n
\n \n \n
\n
\n
\n
\n
\n
\n

阅读历史

\n
最近阅读的书籍,点击可续读
\n
\n \n
\n
\n
\n
\n
\n
\n
\n

净化词(0)· 点✕删除

\n
正文出现净化词,包含它的整句会被清除(阅读和下载TXT都生效)
\n
\n
\n \n \n \n
\n
\n
\n \n \n
\n
\n
\n 净化说明(阅读和下载TXT都会生效)\n \n
\n
\n

1. 整句清除:正文出现净化词,就把它连着的整句删掉——词前面连着的字、词本身、后面连着的字和句末标点(。!?…)全清;前后若遇到标点就停。例:删\"广告\",句子\"本书由广告赞助。\"整句消失。

\n

2. 一次加多个词:用、隔开即可。例:广告、加群、扫码。

\n

3. 只想删词、不误删旁边的字:在词前或词后加/。词前加/(如/广告)=左边的字不误删;词后加/=右边的字和句末标点都不误删;两边都加(如/广告/)=只删\"广告\"二字,最保险。

\n

4. 英文单词不怕被切开:夹在字母中间的/和.算单词的一部分(如 a/b、U.S.不会被切断)。

\n

5. 删除词:点词条右侧的✕即可。

\n
\n
\n
\n
\n
\n
\n
\n

选择下载范围

\n

全书下载

\n
\n
\n
\n
\n
\n
全部章节
\n
全部章节
\n
\n
\n
\n
\n
\n
从当前章开始
\n
当前章 ~ 末尾
\n
\n
\n
\n
\n
\n
自定义范围
\n
\n \n ~\n \n
\n
\n
\n
\n
\n
\n
勾选章节(选择模式)
\n
在目录中手动勾选要下载的章节
\n
\n
\n
\n
\n
正在下载…
\n
\n
0 / 0
\n \n
\n
\n \n \n
\n
\n
\n
\n"; /* ==================== iframe 阅读器脚本 ==================== */ var _sharedSrcCache = ''; var SHARED_SRC = (function () { if (_sharedSrcCache) return _sharedSrcCache; _sharedSrcCache = [showToastIn, getBookRootKey, chineseToNumber, romanToNumber, parseChapterNumber, pickChapterNumber, isPunctChar, isEnglishWord, isEngLetter, parsePurifyWord, markPurifyRun, buildPurifyTrie, purifyText, purifyWordsVersion, purifyDomElement, purifyHtml, normReaderUrl, buildTocIndex, tocTitleForUrlIdx] .map(function (f) { return f.toString(); }).join('\n'); return _sharedSrcCache; })(); function iframeMain() { var m = document.getElementById('topbar'), h = document.getElementById('bottombar'), e = document.getElementById('overlay'), E = document.getElementById('contentWrapper'), g = document.getElementById('articleContainer'), f = document.getElementById('chaptersHost'), p = document.getElementById('loadingTip'), v = document.getElementById('barTitle'), y = document.getElementById('stitchLoadingNext'), n = document.getElementById('catalogSidebar'), r = document.getElementById('catalogMask'), _ = document.getElementById('catalogList'), btnLoadToc = document.getElementById('btnLoadToc'), btnDownloadAll = document.getElementById('btnDownloadAll'), loadingText = document.getElementById('loadingText'), progressTrack = document.getElementById('progressTrack'), progressBar = document.getElementById('progressBar'), tapHint = document.getElementById('tapHint'), fontRange = document.getElementById('fontRange'), fontVal = document.getElementById('fontVal'), themeRow = document.getElementById('themeRow'), fontSegEl = document.getElementById('fontSeg'), tocBookTitle = document.getElementById('tocBookTitle'), tocBookSub = document.getElementById('tocBookSub'), tocBookcover = document.getElementById('tocBookcover'), tocBookProgressBar = document.getElementById('tocBookProgressBar'), whitelistInput = document.getElementById('whitelistInput'), whitelistAddBtn = document.getElementById('whitelistAdd'), purifyExportBtn = document.getElementById('purifyExport'), purifyImportBtn = document.getElementById('purifyImport'), nightIcon = document.getElementById('nightIcon'), nightLabel = document.getElementById('nightLabel'), downloadModal = document.getElementById('downloadModal'), dlOptions = document.getElementById('dlOptions'), dlFrom = document.getElementById('dlFrom'), dlTo = document.getElementById('dlTo'), dlModalSub = document.getElementById('dlModalSub'), dlSubAll = document.getElementById('dlSubAll'), dlSubFrom = document.getElementById('dlSubFrom'), dlProgress = document.getElementById('dlProgress'), dlProgressTitle = document.getElementById('dlProgressTitle'), dlProgressBar = document.getElementById('dlProgressBar'), dlProgressPct = document.getElementById('dlProgressPct'), dlFoot = document.getElementById('dlFoot'), dlCancel = document.getElementById('dlCancel'), dlConfirm = document.getElementById('dlConfirm'), dlStop = document.getElementById('dlStop'); var STR = { readerMode: '阅读模式', chapters: '目录', noChapters: '暂无目录', loading: '正在加载内容...', loadingJump: '正在加载章节...', loadingNext: '正在加载下一章...', nextChapter: '下一章', loadFail: '章节加载失败', tocLoaded: '已加载目录', night: '夜间', day: '日间', download: '下载全部', downloading: '正在下载...' }; document.title = STR.readerMode; loadingText.textContent = STR.loading; var C = [], d = 0; var o = false, G = false; var stitching = false, awaitingNextUrl = '', stitchFails = 0; var tocData = [], tocUrl = ''; var initialChapterUrl = ''; var catalogRenderTimer = null; function cnNum(v) { v = (v || '').replace(/\s+/g, ''); if (/^\d+$/.test(v)) return parseInt(v, 10); var m = { '零': 0, '〇': 0, '○': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 }; var u = { '十': 10, '百': 100, '千': 1000 }; var total = 0, num = 0; for (var i = 0; i < v.length; i++) { var c = v.charAt(i); if (m[c] !== undefined) { num = m[c]; } else if (u[c] !== undefined) { if (num === 0) num = 1; total += num * u[c]; num = 0; } } return total + num; } function findFirstChapter() { var minNum = Infinity, best = null; for (var i = 0; i < tocData.length; i++) { var title = tocData[i].title || ''; var n2 = null; var mt = /第\s*([0-9零一二三四五六七八九十百千两〇○]+)\s*章/.exec(title); if (mt) n2 = cnNum(mt[1]); else if (/^(楔子|序章|引子|前言|序言|开篇|开始)/.test(title)) n2 = 0; if (n2 !== null && n2 < minNum) { minNum = n2; best = tocData[i]; } } return best || (tocData[0] || null); } var tocLoading = false; var s = { fontSize: 18, lineHeight: 1.9, theme: 'warm', simplified: 'off' }; var currentBookKey = ''; var currentBookTitle = ''; var currentHost = ''; var selectMode = false, dlRunning = false, selectedUrls = {}; var selectToolbar = null; var markActivePrev = null; var lastScrollTop = 0; var scrollTicking = false; // E5:checkAutoLoad 缓存 scrollHeight,避免每帧强制同步布局 var _cachedScrollH = 0, _scrollHDirty = true; function invalidateScrollH() { _scrollHDirty = true; } // E12:dirtyChapters 换 Set var dirtyChapters = new Set(); var extraVisible = false; var extraMask = document.getElementById('extraMask'); var purifyWords = []; var purifyVer = ''; var tocIdx = { u0: {}, u1: {}, n: {} }; var TOC_VIRTUAL_THRESHOLD = 500; var tocVirt = null; var tocVirtRowH = 0; var _virtScrollTimer = null; // E2:目录项 DOM 索引,markActive O(1) var _tocItemMap = { u0: Object.create(null), u1: Object.create(null), cn: Object.create(null) }; function rebuildTocItemMap() { _tocItemMap = { u0: Object.create(null), u1: Object.create(null), cn: Object.create(null) }; var items = _.querySelectorAll('.toc-item'); for (var i = 0; i < items.length; i++) { var it = items[i]; var k0 = it.dataset.nk0, k1 = it.dataset.nk1, cn = it.dataset.cn; if (k0 && !_tocItemMap.u0[k0]) _tocItemMap.u0[k0] = it; if (k1 && !_tocItemMap.u1[k1]) _tocItemMap.u1[k1] = it; if (cn && !_tocItemMap.cn[cn]) _tocItemMap.cn[cn] = it; } } function tocVirtPaint() { if (!tocVirt) return; var view = _.querySelector('.toc-virt-view'); if (!view) return; var st = _.scrollTop, h2 = _.clientHeight || 300, rowH = tocVirt.rowH || 40; var start = Math.max(0, Math.floor(st / rowH) - 10); var end = Math.min(tocVirt.total, Math.ceil((st + h2) / rowH) + 10); view.style.top = (start * rowH) + 'px'; view.innerHTML = ''; var frag = document.createDocumentFragment(); for (var i = start; i < end; i++) { frag.appendChild(createItem(tocVirt.items[i].item, i === tocVirt.curIdx)); } view.appendChild(frag); rebuildTocItemMap(); markActive(); } function onVirtScroll() { if (_virtScrollTimer) return; _virtScrollTimer = requestAnimationFrame(function () { _virtScrollTimer = null; tocVirtPaint(); }); } var purifyRefreshTimer = null; var whitelistPanel = document.getElementById('whitelistPanel'); var historyPanel = document.getElementById('historyPanel'); var btnNight = document.getElementById('btnNightMode'); var Y = document.getElementById('settingsPanel'), j = document.getElementById('settingsMask'); function toast(msg) { showToastIn(document, '__reader_toast__', msg, 'position:fixed;left:50%;bottom:calc(env(safe-area-inset-bottom) + 96px);transform:translateX(-50%);z-index:999;max-width:80%;padding:10px 18px;border-radius:999px;background:rgba(26,23,19,.88);color:#F3EDE1;font-size:13px;line-height:1.5;font-family:var(--sans,-apple-system,"PingFang SC",sans-serif);pointer-events:none;', 2200); } function toastWithRetry(msg, onRetry) { try { var old = document.getElementById('__reader_toast_retry__'); if (old && old.parentNode) old.parentNode.removeChild(old); var t = document.createElement('div'); t.id = '__reader_toast_retry__'; t.style.cssText = 'position:fixed;top:18%;left:50%;transform:translateX(-50%);z-index:1000;background:rgba(0,0,0,.8);color:#fff;font-size:13px;line-height:1.6;padding:9px 14px;border-radius:20px;font-family:sans-serif;display:flex;align-items:center;gap:10px;pointer-events:auto;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:80vw;'; var span = document.createElement('span'); span.textContent = msg; var btn = document.createElement('button'); btn.textContent = '重试'; btn.style.cssText = 'background:#3ecf5f;color:#fff;border:none;border-radius:14px;padding:4px 14px;font-size:13px;cursor:pointer;flex-shrink:0;'; btn.addEventListener('click', function () { try { if (t.parentNode) t.parentNode.removeChild(t); } catch (e) {} if (onRetry) onRetry(); }); t.appendChild(span); t.appendChild(btn); (document.body || document.documentElement).appendChild(t); setTimeout(function () { try { if (t.parentNode) t.parentNode.removeChild(t); } catch (e) {} }, 20000); } catch (e) {} } // ====================== 图片过滤函数 ====================== function filterImages(container) { var imgs = container.querySelectorAll('img'); imgs.forEach(function(img) { var src = img.getAttribute('src') || img.getAttribute('data-src') || ''; if (/^data:image\/gif/i.test(src) || /\.(gif|webp|apng)(\?.*)?$/i.test(src)) { img.remove(); return; } var checkAndRemove = function() { var _img = this; var run = function() { var nw = _img.naturalWidth, nh = _img.naturalHeight; if (nw > 0 && nh > 0) { if (nh >= 80) _img.style.maxHeight = 'none'; if (nw > nh || isStretchedBanner(_img)) { _img.remove(); } } }; if (typeof requestAnimationFrame === 'function') { try { requestAnimationFrame(run); return; } catch (e) {} } run(); }; if (img.complete && img.naturalWidth > 0) { checkAndRemove.call(img); return; } img.addEventListener('load', checkAndRemove); img.addEventListener('error', function() { this.remove(); }); }); // E10:CSS 选择器收窄 var els = container.querySelectorAll('[style*="background"]'); els.forEach(function(el) { var st = el.getAttribute('style') || ''; var m2 = /(?:^|;)\s*background(?:-image)?\s*:\s*url\((['"]?)([^'")]+)\1\)/i.exec(st); if (!m2) return; var url = m2[2].trim(); if (!url) return; var im = new Image(); im.onload = function() { try { var nw = this.naturalWidth, nh = this.naturalHeight; if (!(nw > 0 && nh > 0 && nw > nh)) return; var w = el.offsetWidth || 0, h2 = el.offsetHeight || 0; if (!(w > 0 && h2 > 0 && w > h2)) return; el.remove(); } catch (e) {} }; im.onerror = function() {}; im.src = url; }); function isStretchedBanner(img) { var nw = img.naturalWidth, nh = img.naturalHeight; if (!(nw > 0 && nh > 0) || nw > nh) return false; var w = img.offsetWidth || 0, h2 = img.offsetHeight || 0; return w > 0 && h2 > 0 && w > h2 && (w / h2) >= 1.5; } } var lazyImgObserver = null; function lazyLoadImg(img) { var src = img.getAttribute('data-src'); if (!src) return; function unclamp(img2, im) { try { if (im.naturalHeight >= 80) img2.style.maxHeight = 'none'; } catch (e) {} } if (typeof IntersectionObserver === 'undefined') { var im0 = new Image(); im0.onload = function () { img.src = src; img.removeAttribute('data-src'); unclamp(img, im0); invalidateScrollH(); }; im0.onerror = function () { img.removeAttribute('data-src'); }; im0.src = src; return; } if (!lazyImgObserver) { lazyImgObserver = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (!en.isIntersecting) return; var img2 = en.target; var s2 = img2.getAttribute('data-src'); if (!s2) return; var im = new Image(); im.onload = function () { img2.src = s2; img2.removeAttribute('data-src'); unclamp(img2, im); invalidateScrollH(); }; im.onerror = function () { img2.removeAttribute('data-src'); }; im.src = s2; try { lazyImgObserver.unobserve(img2); } catch (e) {} }); }, { rootMargin: '600px 0px' }); } lazyImgObserver.observe(img); } function getHighlightColor() { switch (s.theme) { case 'white': return '#e6f2ff'; case 'warm': return '#e8ddc8'; case 'green': return '#c5e1c8'; case 'dark': return '#3a4a5a'; default: return '#e6f2ff'; } } function send(msg) { try { window.parent.postMessage(msg, '*'); } catch (err) {} } // 带进度上报的当前章节通知 function sendCurrent(url, title) { send({ type: 'setCurrentUrl', url: url || '', title: title || '', bookKey: currentBookKey, bookTitle: currentBookTitle, chapterNo: getCurChapterNo(), chapterTotal: (tocData && tocData.length) || 0 }); } function forwardDiag(level, msg) { try { var text = (typeof msg === 'string') ? msg : (function () { try { return JSON.stringify(msg); } catch (e) { return String(msg); } })(); if (text.length > 600) text = text.slice(0, 600) + '…'; send({ type: '__diag', level: level, msg: text }); } catch (e) {} } try { ['log', 'info', 'warn', 'error', 'debug'].forEach(function (m2) { var orig = console[m2]; if (typeof orig !== 'function') return; console[m2] = function () { try { var args = Array.prototype.slice.call(arguments); var text = args.map(function (a) { return (typeof a === 'string') ? a : (function () { try { return JSON.stringify(a); } catch (e) { return String(a); } })(); }).join(' '); if (m2 === 'error' || m2 === 'warn' || text.charAt(0) === '[') forwardDiag(m2.toUpperCase(), text); } catch (e) {} return orig.apply(console, arguments); }; }); window.addEventListener('error', function (ev) { forwardDiag('ERROR', 'iframe window.onerror: ' + (ev.message || '') + ' @ ' + (ev.filename || '') + ':' + (ev.lineno || '')); }, true); } catch (e) {} // E1:加 source 校验 window.addEventListener('message', function (ev) { var msg = ev.data; if (!msg || typeof msg !== 'object' || !msg.type) return; handleMessage(msg); }); function handleMessage(msg) { switch (msg.type) { case 'init': if (msg.settings && typeof msg.settings === 'object') { if (msg.settings.fontSize) s.fontSize = msg.settings.fontSize; if (msg.settings.lineHeight) s.lineHeight = msg.settings.lineHeight; if (msg.settings.theme) s.theme = msg.settings.theme; if (msg.settings.font) s.font = msg.settings.font; if (msg.settings.simplified) s.simplified = msg.settings.simplified; if (s.theme === 'paper') s.theme = 'white'; } currentBookKey = msg.bookKey || ''; currentBookTitle = msg.bookTitle || ''; currentHost = msg.host || ''; if (Array.isArray(msg.purify)) { purifyWords = msg.purify; purifyVer = purifyWordsVersion(purifyWords); } applySettings(); { var style = document.createElement('style'); style.id = 'inline-img-style'; style.textContent = '\n .chapter-content p img {\n max-height: 1.6em;\n vertical-align: middle;\n margin: 0 2px;\n }\n '; document.head.appendChild(style); } tocData = msg.toc || []; tocUrl = msg.tocUrl || ''; tocIdx = buildTocIndex(tocData); btnLoadToc.style.display = tocUrl ? '' : 'none'; if (msg.chapter) { initialChapterUrl = msg.chapter.url || ''; J(msg.chapter, true); I(msg.chapter); } updateTocUI(); refreshRenderedTitles(); fillBookInfo(); if (tocUrl && (!tocData || tocData.length === 0) && !tocLoading) { startTocFetch(); } setTimeout(function () { setBars(false); try { tapHint.classList.add('show'); } catch (e) {} setTimeout(function () { try { tapHint.classList.remove('show'); } catch (e2) {} }, 2000); }, 2200); break; case 'setBookInfo': currentBookKey = msg.bookKey || currentBookKey; currentBookTitle = msg.bookTitle || currentBookTitle; if (typeof updateTocUI === 'function') updateTocUI(); fillBookInfo(); break; case 'themeUpdate': if (msg.theme && /^(white|warm|green|dark)$/.test(msg.theme)) { s.theme = msg.theme; applySettings(); } break; case 'convertUpdate': { if (typeof msg.mode === 'string') { s.simplified = msg.mode; var curUrl = currentUrl(); if (curUrl) { C = []; d = 0; f.innerHTML = ''; stitching = false; awaitingNextUrl = ''; stitchFails = 0; dirtyChapters = new Set(); invalidateScrollH(); showLoading(STR.loadingJump); send({ type: 'fetchChapter', url: curUrl, mode: 'jump' }); } } break; } case 'reinit': { C = []; f.innerHTML = ''; stitching = false; awaitingNextUrl = ''; stitchFails = 0; dirtyChapters = new Set(); invalidateScrollH(); if (Array.isArray(msg.purify)) { purifyWords = msg.purify; purifyVer = purifyWordsVersion(purifyWords); } if (msg.chapter) { initialChapterUrl = msg.chapter.url || ''; J(msg.chapter, true); I(msg.chapter); } fillBookInfo(); try { toast('已应用新的净化规则'); } catch (e1) {} break; } case 'chapterReady': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); resetAndLoad(msg.chapter); } else { if (awaitingNextUrl && msg.url === awaitingNextUrl) { awaitingNextUrl = ''; stitching = false; J(msg.chapter, false); stitchFails = 0; setTimeout(checkAutoLoad, 0); } else { try { console.warn('[阅读模式] 自动加载状态异常:等待', awaitingNextUrl || '(空)', '但收到', msg.url || '(空)'); } catch (e1) {} } } fillBookInfo(); break; case 'chapterFailed': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); toast(STR.loadFail + (msg.reason ? ':' + msg.reason : '')); } else { stitching = false; awaitingNextUrl = ''; stitchFails++; if (stitchFails < 3) setTimeout(checkAutoLoad, 2000); else { try { toastWithRetry('下一页加载失败:' + (msg.reason || '') , function () { stitchFails = 0; checkAutoLoad(); }); } catch (e1) {} } try { console.warn('[阅读模式] 章节自动加载失败(第' + (stitchFails) + '次):', msg.url || '', msg.reason || ''); } catch (e1) {} } break; case 'tocReady': tocData = msg.toc || []; tocIdx = buildTocIndex(tocData); tocLoading = false; resetTocBtn(); renderCatalog(true); updateTocUI(); refreshRenderedTitles(); fillBookInfo(); if (msg.fromFallback && tocData && tocData.length) { var _fbCur = (C.length && d < C.length) ? C[d].url : ''; var _fbFirst = findFirstChapter(); if (_fbFirst && _fbFirst.url && _fbCur && _fbCur === initialChapterUrl && _fbFirst.url !== _fbCur) { closeCatalog(); showLoading(STR.loadingJump); send({ type: 'fetchChapter', url: _fbFirst.url, mode: 'jump' }); } if (!_.querySelector('.toc-item.active') && _fbFirst && _fbFirst.url) { var fbItems = _.querySelectorAll('.toc-item'); var fbTarget = null; for (var _fi = 0; _fi < fbItems.length; _fi++) { if (fbItems[_fi].dataset.tocUrl === _fbFirst.url) { fbTarget = fbItems[_fi]; break; } } var fbFirstEl = fbTarget || _.querySelector('.toc-item'); if (fbFirstEl) { fbFirstEl.classList.add('active'); markActivePrev = fbFirstEl; } } } if (n.classList.contains('visible')) scrollActiveIntoCenter(); break; case 'tocProgress': break; case 'whitelistData': renderWhitelist(msg.list || []); break; case 'purifyUpdate': purifyWords = Array.isArray(msg.purify) ? msg.purify : []; purifyVer = purifyWordsVersion(purifyWords); if (purifyRefreshTimer) clearTimeout(purifyRefreshTimer); purifyRefreshTimer = setTimeout(reapplyPurify, 300); break; case 'purifyData': renderPurify(msg.list || []); break; case 'historyData': renderHistory(msg.list || []); break; case 'downloadStopping': break; case 'downloadComplete': resetDlBtn(); closeDownloadModal(); toast('下载完成!'); break; case 'downloadPaused': resetDlBtn(); closeDownloadModal(); toast('已停止下载,保存了 ' + msg.current + ' 章'); break; case 'downloadProgress': btnDownloadAll.textContent = '下载中 ' + msg.current + '/' + msg.total; updateDownloadProgress(msg.current, msg.total); break; } } function showContent(title) { v.textContent = title || STR.readerMode; p.style.display = 'none'; g.style.display = 'block'; } function showLoading(text) { loadingText.textContent = text || STR.loading; p.style.display = 'flex'; g.style.display = 'none'; } function hideLoading() { p.style.display = 'none'; g.style.display = 'block'; } function hideBars() { setBars(false); } function setBars(visible) { o = visible; m.classList.toggle('hidden', !visible); h.classList.toggle('hidden', !visible); progressTrack.classList.toggle('hidden', !visible); } function sanitize(el) { var bad = el.querySelectorAll('script,iframe,object,embed,link,meta,form,input,button,select,textarea'); for (var i = 0; i < bad.length; i++) { try { bad[i].parentNode.removeChild(bad[i]); } catch (e2) {} } try { var all = el.querySelectorAll('*'); for (var a = 0; a < all.length; a++) { var node = all[a]; var attrs = node.attributes; if (!attrs || !attrs.length) continue; for (var k = attrs.length - 1; k >= 0; k--) { var attr = attrs[k]; var attrName = attr.name.toLowerCase(); if (/^on/i.test(attrName)) { node.removeAttribute(attr.name); continue; } var val = String(attr.value || '').replace(/[\u0000-\u0020]+/g, '').toLowerCase(); if ((attrName === 'href' || attrName === 'src' || attrName === 'xlink:href' || attrName === 'action' || attrName === 'formaction') && (/^javascript:/i.test(val) || /^vbscript:/i.test(val) || /^data:text\/html/i.test(val))) { node.removeAttribute(attr.name); } } var st = node.getAttribute ? node.getAttribute('style') : null; if (st && /expression\s*\(/i.test(st)) node.removeAttribute('style'); } } catch (e3) {} } function stripPageSuffix(t) { return String(t || '') .replace(/[((【\[]\s*第?\s*[0-9一二三四五六七八九十百千]+\s*\/\s*[0-9一二三四五六七八九十百千]+\s*页?\s*[))】\]]/g, '') .replace(/[((【\[]\s*第\s*[0-9一二三四五六七八九十百千万亿两]+\s*页\s*[))】\]]/g, '') .replace(/第\s*[0-9一二三四五六七八九十百千万亿两]+\s*页$/g, '') .replace(/\s+/g, '') .trim(); } function sameChapterTitle(a, b) { if (!a || !b) return false; var ca = String(a).replace(/\s+/g, '').trim(); var cb = String(b).replace(/\s+/g, '').trim(); if (!ca || !cb) return false; if (ca === cb) return true; var n1 = parseChapterNumber(ca); var n2 = parseChapterNumber(cb); if (n1 !== null && n2 !== null && n1 === n2) return true; var ra = stripPageSuffix(ca); var rb = stripPageSuffix(cb); return ra.length > 0 && ra === rb; } function J(t, first) { if (!t || !t.url) return; var dispTitle = tocTitleForUrlIdx(tocIdx, t.url, t.title) || t.title || ''; var skipPurify = (typeof t.purifyVersion === 'string' && t.purifyVersion === purifyVer); var prev = C.length ? C[C.length - 1] : null; var isPage = !first && prev && sameChapterTitle(prev.title, dispTitle); if (isPage) { var cont = prev.element.querySelector('.chapter-content'); if (cont) { var tmp = document.createElement('div'); tmp.innerHTML = skipPurify ? (t.data || '') : purifyHtml(t.data || '', purifyWords); sanitize(tmp); filterImages(tmp); while (tmp.firstChild) cont.appendChild(tmp.firstChild); var imgs = tmp.querySelectorAll('img[data-src]'); for (var i = 0; i < imgs.length; i++) lazyLoadImg(imgs[i]); } prev.url = t.url || prev.url; prev.nextUrl = t.next_url || prev.nextUrl; invalidateScrollH(); scheduleCatalogRender(); return; } var block = document.createElement('div'); block.className = 'chapter-block'; block.dataset.url = t.url || ''; if (!first) { var divider = document.createElement('div'); divider.className = 'chapter-divider'; var dt = document.createElement('span'); dt.className = 'chapter-divider-title'; dt.textContent = dispTitle || STR.nextChapter; divider.appendChild(dt); block.appendChild(divider); } var titleEl = document.createElement('div'); titleEl.className = 'chapter-title'; titleEl.textContent = dispTitle; block.appendChild(titleEl); var contentEl = document.createElement('div'); contentEl.className = 'chapter-content'; contentEl.innerHTML = skipPurify ? (t.data || '') : purifyHtml(t.data || '', purifyWords); sanitize(contentEl); filterImages(contentEl); block.appendChild(contentEl); var endEl = document.createElement('div'); endEl.className = 'chapter-end'; var orn = document.createElement('div'); orn.className = 'ornament'; orn.textContent = '· · ·'; var endMeta = document.createElement('div'); endMeta.className = 'end-meta'; endMeta.textContent = '本章完'; endEl.appendChild(orn); endEl.appendChild(endMeta); block.appendChild(endEl); f.appendChild(block); invalidateScrollH(); C.push({ url: t.url, title: dispTitle, nextUrl: t.next_url || '', element: block }); var imgs2 = block.querySelectorAll('img[data-src]'); for (var i2 = 0; i2 < imgs2.length; i2++) lazyLoadImg(imgs2[i2]); if (first) { d = 0; showContent(dispTitle); E.scrollTop = 0; sendCurrent(t.url, dispTitle); } if (n.classList.contains('visible')) scheduleCatalogRender(); } function refreshRenderedTitles() { var i, item, tocT, blk, tEl, dEl; for (i = 0; i < C.length; i++) { item = C[i]; tocT = tocTitleForUrlIdx(tocIdx, item.url, item.title); if (!tocT || tocT === item.title) continue; item.title = tocT; blk = item.element; if (!blk) continue; tEl = blk.querySelector('.chapter-title'); if (tEl) tEl.textContent = tocT; dEl = blk.querySelector('.chapter-divider-title'); if (dEl) dEl.textContent = tocT; } if (C.length && C[d]) { v.textContent = C[d].title || STR.readerMode; sendCurrent(C[d].url, C[d].title); } } function I(t) { if (t && t.next_url) M(t.next_url); } function findLoadedByUrl(url) { if (!url) return -1; for (var i = 0; i < C.length; i++) if (C[i].url === url) return i; return -1; } function M(nextUrl) { if (!nextUrl || stitching) return; if (findLoadedByUrl(nextUrl) !== -1) return; stitching = true; awaitingNextUrl = nextUrl; y.classList.add('visible'); send({ type: 'fetchChapter', url: nextUrl, mode: 'stitch' }); } function resetAndLoad(chapter) { C = []; d = 0; f.innerHTML = ''; stitching = false; awaitingNextUrl = ''; stitchFails = 0; y.classList.remove('visible'); invalidateScrollH(); J(chapter, true); I(chapter); E.scrollTop = 0; } function updateTocUI() { btnDownloadAll.textContent = tocLoading ? '加载中' : STR.download; } function getCurChapterNo() { try { var hit = _.querySelector('.toc-item.active'); if (hit && hit.dataset && hit.dataset.cn) { var n0 = parseInt(hit.dataset.cn, 10); if (n0 > 0) return n0; } } catch (e) {} var curC = (C && C.length && C[d]) ? C[d] : null; if (curC && curC.title) { var n2 = parseChapterNumber(curC.title); if (n2 !== null && n2 > 0) return n2; } if (tocData && tocData.length && curC && curC.url) { var _ln = normReaderUrl(curC.url); for (var _i = 0; _i < tocData.length; _i++) { var _tu = tocData[_i] && tocData[_i].url ? normReaderUrl(tocData[_i].url) : null; if (_tu && (_tu[0] === _ln[0] || _tu[1] === _ln[1] || _tu[0] === _ln[1] || _tu[1] === _ln[0])) return _i + 1; } } if (C && C.length) return C.length; return 0; } function updateBookSub() { var total = tocData && tocData.length ? tocData.length : (C.length || 0); var cur = getCurChapterNo(); if (tocBookSub) { tocBookSub.textContent = '共 ' + (total || '?') + ' 章' + (cur ? ' · 已读到第 ' + cur + ' 章' : ''); } if (tocBookProgressBar) { var totalToc = tocData && tocData.length ? tocData.length : 0; var pct = (totalToc > 0 && cur > 0) ? Math.max(0, Math.min(100, Math.round(cur / totalToc * 100))) : 0; tocBookProgressBar.style.width = pct + '%'; } } function fillBookInfo() { var title = currentBookTitle || '未命名'; if (tocBookTitle) tocBookTitle.textContent = title; if (tocBookcover) tocBookcover.textContent = (title || '').slice(0, 4); updateBookSub(); } function setTocBtnStopping() { btnLoadToc.textContent = '停止'; btnLoadToc.style.opacity = '1'; btnLoadToc.style.pointerEvents = 'auto'; btnLoadToc.style.cursor = 'pointer'; } function resetTocBtn() { btnLoadToc.textContent = '加载目录页'; btnLoadToc.style.opacity = ''; btnLoadToc.style.pointerEvents = ''; btnLoadToc.style.cursor = ''; } function startTocFetch() { tocLoading = true; updateTocUI(); setTocBtnStopping(); send({ type: 'fetchToc', url: tocUrl }); } function catalogItems() { if (tocData && tocData.length) return tocData; return C.map(function (c, i) { return { title: c.title || ('章节 ' + (i + 1)), url: c.url }; }); } function currentUrl() { return C.length && C[d] ? C[d].url : ''; } function createItem(it, isActive) { var el = document.createElement('div'); el.className = 'toc-item' + (isActive ? ' active' : ''); if (it.url) el.dataset.tocUrl = it.url; var _nk = it.url ? normReaderUrl(it.url) : null; if (_nk) { el.dataset.nk0 = _nk[0]; el.dataset.nk1 = _nk[1]; } var _cn = parseChapterNumber(it.title || ''); if (_cn !== null && _cn > 0) el.dataset.cn = String(_cn); if (selectMode) { el.style.display = 'flex'; el.style.alignItems = 'center'; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'dl-check'; cb.dataset.dlUrl = it.url || ''; cb.checked = !!selectedUrls[it.url]; cb.style.cssText = 'margin:0 8px 0 0;flex:none;width:16px;height:16px;cursor:pointer;'; cb.addEventListener('click', function (ev) { ev.stopPropagation(); }); cb.addEventListener('change', function () { if (cb.checked) selectedUrls[it.url] = true; else delete selectedUrls[it.url]; updateSelCount(); }); el.appendChild(cb); var _st = document.createElement('span'); _st.className = 'tt'; _st.textContent = it.title; el.appendChild(_st); el.addEventListener('click', function () { cb.checked = !cb.checked; if (cb.checked) selectedUrls[it.url] = true; else delete selectedUrls[it.url]; updateSelCount(); }); return el; } var idxEl = document.createElement('span'); idxEl.className = 'idx num'; idxEl.textContent = (_cn !== null && _cn > 0) ? _cn : ''; el.appendChild(idxEl); var ttEl = document.createElement('span'); ttEl.className = 'tt'; ttEl.textContent = it.title; el.appendChild(ttEl); if (findLoadedByUrl(it.url) !== -1) { el.classList.add('done'); var tagEl = document.createElement('span'); tagEl.className = 'tag'; tagEl.textContent = '已读'; el.appendChild(tagEl); } el.addEventListener('click', function () { closeCatalog(); var idx = findLoadedByUrl(it.url); if (idx >= 0) { var c = C[idx]; if (c && c.element) { c.element.scrollIntoView({ behavior: 'smooth', block: 'start' }); d = idx; v.textContent = c.title || STR.readerMode; sendCurrent(c.url, c.title); markActive(); fillBookInfo(); } } else if (it.url) { showLoading(STR.loadingJump); send({ type: 'fetchChapter', url: it.url, mode: 'jump' }); } }); return el; } // E3:签名跳过重复重建 var _lastCatalogSig = ''; function computeCatalogSig(kw) { var items = catalogItems(); var sStr = 'kw:' + (kw || '') + '|sel:' + selectMode + '|'; for (var i = 0; i < items.length; i++) sStr += (items[i].url || '') + '\u0001' + (items[i].title || '') + '\u0002'; var h2 = 5381; for (var j = 0; j < sStr.length; j++) h2 = ((h2 << 5) + h2 + sStr.charCodeAt(j)) >>> 0; return h2.toString(36) + ':' + items.length; } function renderCatalog(force) { if (catalogRenderTimer) { clearTimeout(catalogRenderTimer); catalogRenderTimer = null; } var keepScroll = _.scrollTop; var kw = ''; try { kw = (document.getElementById('tocSearch').value || '').trim().toLowerCase(); } catch (e) {} var sig = computeCatalogSig(kw); if (!force && sig === _lastCatalogSig && _.querySelector('.toc-item')) { rebuildTocItemMap(); markActive(); return; } _lastCatalogSig = sig; _.innerHTML = ''; var items = catalogItems(); if (kw) { items = items.filter(function (it) { var t = (it.title || '').toLowerCase(); if (t.indexOf(kw) !== -1) return true; var n2 = parseChapterNumber(it.title || ''); if (n2 !== null && String(n2).indexOf(kw) !== -1) return true; return false; }); } if (!items.length) { tocVirt = null; _.innerHTML = '
' + STR.noChapters + '
'; _.scrollTop = keepScroll; return; } var seen = {}, unique = []; items.forEach(function (it) { if (!it.url) return; if (seen[it.url]) return; seen[it.url] = true; unique.push(it); }); items = unique; var cur = currentUrl(); var hasVolumeAndChapter = items.some(function (it) { var t = it.title || ''; return /卷.*章/.test(t); }); var normal = [], special = []; items.forEach(function (it) { var num = parseChapterNumber(it.title); if (num !== null && num > 0) { normal.push({ item: it, num: num }); } else { special.push(it); } }); if (!selectMode && !hasVolumeAndChapter && special.length === 0 && normal.length > TOC_VIRTUAL_THRESHOLD) { renderCatalogVirtual(normal, cur, keepScroll); return; } tocVirt = null; _.removeEventListener('scroll', onVirtScroll); var frag = document.createDocumentFragment(); if (hasVolumeAndChapter) { items.forEach(function (it) { frag.appendChild(createItem(it, it.url === cur)); }); _.appendChild(frag); rebuildTocItemMap(); markActive(); _.scrollTop = keepScroll; return; } normal.sort(function (a, b) { return a.num - b.num; }); normal.forEach(function (o2) { frag.appendChild(createItem(o2.item, o2.item.url === cur)); }); if (special.length) { var toggleBtn = document.createElement('div'); toggleBtn.className = 'catalog-toggle'; toggleBtn.textContent = '▼ 其他章节 (' + special.length + ')'; toggleBtn.style.cssText = 'padding:8px 20px;font-size:13px;opacity:.6;cursor:pointer;border-top:1px solid rgba(128,128,128,.2);margin-top:8px;user-select:none;'; toggleBtn.addEventListener('click', function (ev) { ev.stopPropagation(); var container = this.nextElementSibling; if (container.style.display === 'none') { container.style.display = 'block'; this.textContent = '▲ 其他章节 (' + special.length + ')'; } else { container.style.display = 'none'; this.textContent = '▼ 其他章节 (' + special.length + ')'; } }); frag.appendChild(toggleBtn); var specialContainer = document.createElement('div'); specialContainer.className = 'special-chapters'; specialContainer.style.display = 'none'; special.forEach(function (it) { specialContainer.appendChild(createItem(it, it.url === cur)); }); frag.appendChild(specialContainer); } _.appendChild(frag); rebuildTocItemMap(); markActive(); _.scrollTop = keepScroll; } function scheduleCatalogRender() { if (tocData && tocData.length) return; if (!n.classList.contains('visible')) return; if (catalogRenderTimer) return; catalogRenderTimer = setTimeout(function () { catalogRenderTimer = null; try { renderCatalog(true); } catch (e) {} }, 150); } function renderCatalogVirtual(normalList, cur, keepScroll) { var total = normalList.length; if (!tocVirtRowH) { var probe = createItem(normalList[0].item, false); _.appendChild(probe); tocVirtRowH = probe.offsetHeight || 40; _.removeChild(probe); } var rowH = tocVirtRowH; var curIdx = -1; var curNorm = cur ? normReaderUrl(cur) : null; var curTitle = (C.length && C[d]) ? (C[d].title || '') : ''; var curBn = null; if (curTitle) { curBn = parseChapterNumber(curTitle); if (curBn !== null && curBn <= 0) curBn = null; } if (curNorm || curBn !== null) { for (var i = 0; i < total; i++) { var it = normalList[i].item; if (curNorm) { var t = it.url ? normReaderUrl(it.url) : null; if (t && (t[0] === curNorm[0] || t[0] === curNorm[1] || t[1] === curNorm[0] || t[1] === curNorm[1])) { curIdx = i; break; } } if (curIdx === -1 && curBn !== null) { var cn2 = parseChapterNumber(it.title || ''); if (cn2 !== null && cn2 === curBn) { curIdx = i; break; } } } } tocVirt = { rowH: rowH, total: total, curIdx: curIdx, items: normalList }; var spacer = document.createElement('div'); spacer.style.cssText = 'position:relative;height:' + (total * rowH) + 'px;width:100%;'; var view = document.createElement('div'); view.className = 'toc-virt-view'; view.style.cssText = 'position:absolute;left:0;right:0;top:0;'; spacer.appendChild(view); _.appendChild(spacer); if (typeof keepScroll === 'number' && keepScroll > 0) { _.scrollTop = keepScroll; } tocVirtPaint(); _.removeEventListener('scroll', onVirtScroll); _.addEventListener('scroll', onVirtScroll); } // E2:markActive Map 查表 function markActive() { var cur = currentUrl(); var curTitle = (C.length && C[d]) ? (C[d].title || '') : ''; var hit = null; var curNorm = cur ? normReaderUrl(cur) : null; var curBn = null; if (curTitle) { curBn = parseChapterNumber(curTitle); if (curBn !== null && curBn <= 0) curBn = null; } if (curNorm) { var a = curNorm[0], b = curNorm[1]; hit = _tocItemMap.u0[a] || _tocItemMap.u0[b] || _tocItemMap.u1[a] || _tocItemMap.u1[b] || null; } if (!hit && curBn !== null) hit = _tocItemMap.cn[curBn] || null; if (hit === markActivePrev) { updateBookSub(); return; } if (markActivePrev) { try { markActivePrev.classList.remove('active'); } catch (e) {} } markActivePrev = null; var old = _.querySelector('.toc-item.active'); if (old && old !== hit) { try { old.classList.remove('active'); } catch (e) {} } if (hit) { hit.classList.add('active'); markActivePrev = hit; } updateBookSub(); } function scrollActiveIntoCenter() { if (tocVirt && tocVirt.curIdx >= 0 && tocVirt.rowH) { var y2 = tocVirt.curIdx * tocVirt.rowH; _.scrollTop = Math.max(0, y2 - (_.clientHeight - tocVirt.rowH) / 2); return; } var act = _.querySelector('.toc-item.active'); if (!act) return; var sp = act.parentElement; while (sp && sp !== _ && !(sp.classList && sp.classList.contains('special-chapters'))) sp = sp.parentElement; if (sp && sp !== _ && sp.style.display === 'none') { sp.style.display = 'block'; var tg = sp.previousElementSibling; if (tg && tg.classList && tg.classList.contains('catalog-toggle')) { tg.textContent = '▲ 其他章节 (' + sp.children.length + ')'; } } var r2 = act.getBoundingClientRect(), lr = _.getBoundingClientRect(); var cur2 = r2.top - lr.top; var want = (lr.height - r2.height) / 2; _.scrollTop += (cur2 - want); } function openCatalog() { renderCatalog(); n.classList.add('visible'); r.classList.add('visible'); hideBars(); scrollActiveIntoCenter(); if (tocData.length === 0 && tocUrl && !tocLoading) startTocFetch(); } function closeCatalog() { n.classList.remove('visible'); r.classList.remove('visible'); } // E5:缓存 scrollHeight function checkAutoLoad() { if (C.length === 0 || stitching) return; if (_scrollHDirty) { _cachedScrollH = E.scrollHeight; _scrollHDirty = false; } if (_cachedScrollH - (E.scrollTop + E.clientHeight) < 800) { var last = C[C.length - 1]; if (last && last.nextUrl) M(last.nextUrl); } } function reapplyPurify() { purifyRefreshTimer = null; var cs = f.querySelectorAll('.chapter-content'); if (!cs.length) return; var viewBottom = (E.clientHeight || window.innerHeight) + 600; for (var pi = 0; pi < cs.length; pi++) { var el = cs[pi]; if (!el) continue; var r2 = el.getBoundingClientRect(); if (r2.bottom < -600 || r2.top > viewBottom) { dirtyChapters.add(pi); continue; } purifyDomElement(el, purifyWords); dirtyChapters.delete(pi); } invalidateScrollH(); } // E12:Set 遍历 function purgeDirtyOnScroll() { if (!dirtyChapters || !dirtyChapters.size) return; var cs = f.querySelectorAll('.chapter-content'); var viewBottom = (E.clientHeight || window.innerHeight) + 600; var toDelete = []; dirtyChapters.forEach(function (di) { var el = cs[di]; if (el && el.getBoundingClientRect().top <= viewBottom) { purifyDomElement(el, purifyWords); toDelete.push(di); } }); for (var i = 0; i < toDelete.length; i++) dirtyChapters.delete(toDelete[i]); } function onScroll() { var st = E.scrollTop; if (Math.abs(st - lastScrollTop) > 2 && o) { setBars(false); } var prevScrollTop = lastScrollTop; lastScrollTop = st; var _maxS = E.scrollHeight - E.clientHeight; progressBar.style.width = (_maxS > 0 ? Math.min(1, st / _maxS) : 0) * 100 + '%'; checkAutoLoad(); if (!C.length) return; var i, target = -1; if (st >= prevScrollTop) { for (i = (d < 0 ? 0 : d); i < C.length; i++) { if (C[i].element.getBoundingClientRect().top <= 80) target = i; else break; } } else if (d >= 0) { for (i = d; i >= 0; i--) { if (C[i].element.getBoundingClientRect().top <= 80) { target = i; break; } } } if (target !== -1 && d !== target) { d = target; v.textContent = C[target].title || STR.readerMode; sendCurrent(C[target].url, C[target].title); if (n.classList.contains('visible')) markActive(); } purgeDirtyOnScroll(); } E.addEventListener('scroll', function () { if (scrollTicking) return; scrollTicking = true; requestAnimationFrame(function () { scrollTicking = false; onScroll(); }); }); function toggleBars() { if (G) { closeSettings(); return; } if (n.classList.contains('visible')) { closeCatalog(); return; } if (extraVisible) { closeExtraPanel(); return; } setBars(!o); } E.addEventListener('click', function (ev) { var aEl = (ev.target && ev.target.closest) ? ev.target.closest('a') : null; if (aEl) { ev.preventDefault(); var href = aEl.getAttribute('href') || ''; if (href.charAt(0) === '#') { var targetId = href.slice(1); var targetEl = null; if (targetId) { try { targetEl = document.getElementById(targetId); } catch (e) {} } if (targetEl) { try { targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e2) { try { targetEl.scrollIntoView(); } catch (e3) {} } } else { try { E.scrollTop = 0; } catch (e4) {} } return; } var hrefLower = href.toLowerCase(); if (hrefLower.indexOf('javascript:') === 0 || hrefLower.indexOf('about:') === 0 || hrefLower.indexOf('mailto:') === 0) return; var absUrl = ''; try { absUrl = new URL(href, document.baseURI || location.href).href; } catch (e) { absUrl = href; } if (absUrl && absUrl !== location.href) { send({ type: 'navigateTo', url: absUrl }); } return; } if (o || G) { toggleBars(); return; } var rect = E.getBoundingClientRect(); var yy = ev.clientY - rect.top, hh = rect.height, step = hh - 40; if (yy < hh * 0.3) E.scrollBy({ top: -step, behavior: 'smooth' }); else if (yy > hh * 0.7) E.scrollBy({ top: step, behavior: 'smooth' }); else toggleBars(); }); e.addEventListener('click', function () { toggleBars(); }); document.getElementById('btnBack').addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'exit' }); }); document.getElementById('btnCatalog').addEventListener('click', function (ev) { ev.stopPropagation(); openCatalog(); }); r.addEventListener('click', function () { closeCatalog(); }); var tocSearchEl = document.getElementById('tocSearch'); if (tocSearchEl) { var tocSearchTimer = null; tocSearchEl.addEventListener('input', function () { if (tocSearchTimer) clearTimeout(tocSearchTimer); tocSearchTimer = setTimeout(function () { tocSearchTimer = null; renderCatalog(true); if (tocSearchEl.value) { var act = _.querySelector('.toc-item.active'); if (act) { try { act.scrollIntoView({ block: 'center' }); } catch (e) {} } } }, 300); }); } btnLoadToc.addEventListener('click', function (ev) { ev.stopPropagation(); if (!tocUrl) return; if (tocLoading) { send({ type: 'stopFetchToc' }); return; } startTocFetch(); }); function catalogMap() { var map = {}; catalogItems().forEach(function (it) { if (it.url) map[it.url] = it.title || it.url; }); return map; } function allChecks() { return Array.prototype.slice.call(_.querySelectorAll('.dl-check')); } function countSelected() { var cnt = 0; allChecks().forEach(function (c) { if (c.checked) cnt++; }); return cnt; } function updateSelCount() { var nSel = countSelected(); var btn = document.getElementById('dlSelStart'); if (btn) btn.textContent = '开始下载 (' + nSel + ')'; var all = document.getElementById('dlSelAll'); if (all) { var checks = allChecks(); all.checked = checks.length > 0 && nSel === checks.length; } } function toggleAllChecks(on) { allChecks().forEach(function (cb) { cb.checked = on; if (on) selectedUrls[cb.dataset.dlUrl] = true; else delete selectedUrls[cb.dataset.dlUrl]; }); updateSelCount(); } function selectRange(a, b) { var checks = allChecks(); toggleAllChecks(false); var i0 = Math.max(1, a) - 1; var i1 = Math.min(checks.length, b); for (var i = i0; i < i1; i++) { checks[i].checked = true; selectedUrls[checks[i].dataset.dlUrl] = true; } updateSelCount(); } function resetDlBtn() { dlRunning = false; btnDownloadAll.textContent = '下载全部'; btnDownloadAll.disabled = false; } function removeSelectToolbar() { if (selectToolbar && selectToolbar.parentNode) selectToolbar.parentNode.removeChild(selectToolbar); selectToolbar = null; } function mkDlBtn(text, primary) { var btn = document.createElement('button'); btn.textContent = text; btn.style.cssText = primary ? 'padding:5px 12px;border:none;border-radius:6px;background:#d9822b;color:#fff;cursor:pointer;font-size:12px;font-weight:500;' : 'padding:5px 10px;border:1px solid rgba(128,128,128,.4);border-radius:6px;background:transparent;cursor:pointer;font-size:12px;color:inherit;'; return btn; } function buildSelectToolbar() { removeSelectToolbar(); var tb = document.createElement('div'); tb.id = 'dlSelectToolbar'; tb.style.cssText = 'padding:8px 12px;border-bottom:1px solid rgba(128,128,128,.25);display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:13px;flex:none;'; var lab = document.createElement('label'); lab.style.cssText = 'display:flex;align-items:center;gap:4px;cursor:pointer;'; var cbAll = document.createElement('input'); cbAll.type = 'checkbox'; cbAll.id = 'dlSelAll'; cbAll.style.cssText = 'width:15px;height:15px;cursor:pointer;'; cbAll.addEventListener('change', function () { toggleAllChecks(cbAll.checked); }); lab.appendChild(cbAll); lab.appendChild(document.createTextNode('全选')); tb.appendChild(lab); var sp1 = document.createElement('span'); sp1.textContent = '从'; tb.appendChild(sp1); var inpFrom = document.createElement('input'); inpFrom.type = 'number'; inpFrom.min = '1'; inpFrom.value = '1'; inpFrom.style.cssText = 'width:54px;padding:3px 4px;border:1px solid rgba(128,128,128,.4);border-radius:4px;background:transparent;color:inherit;font-size:12px;'; tb.appendChild(inpFrom); var sp2 = document.createElement('span'); sp2.textContent = '到'; tb.appendChild(sp2); var inpTo = document.createElement('input'); inpTo.type = 'number'; inpTo.min = '1'; inpTo.value = '1'; inpTo.style.cssText = inpFrom.style.cssText; tb.appendChild(inpTo); var btnRange = mkDlBtn('选范围', false); btnRange.addEventListener('click', function (ev) { ev.stopPropagation(); var a = parseInt(inpFrom.value, 10) || 1; var b = parseInt(inpTo.value, 10) || 1; if (a > b) { var t = a; a = b; b = t; } selectRange(a, b); }); tb.appendChild(btnRange); var btnStart = mkDlBtn('开始下载 (0)', true); btnStart.id = 'dlSelStart'; btnStart.addEventListener('click', function (ev) { ev.stopPropagation(); startSelectedDownload(); }); tb.appendChild(btnStart); var btnCancel = mkDlBtn('取消', false); btnCancel.addEventListener('click', function (ev) { ev.stopPropagation(); exitSelectMode(); }); tb.appendChild(btnCancel); if (_.parentNode) _.parentNode.insertBefore(tb, _); selectToolbar = tb; } function enterSelectMode() { selectMode = true; selectedUrls = {}; btnDownloadAll.textContent = '选择章节...'; btnDownloadAll.disabled = true; buildSelectToolbar(); renderCatalog(true); } function exitSelectMode() { selectMode = false; selectedUrls = {}; removeSelectToolbar(); btnDownloadAll.textContent = STR.download; btnDownloadAll.disabled = false; renderCatalog(true); } function startSelectedDownload() { var checks = allChecks(); var map = catalogMap(); var list = []; checks.forEach(function (cb) { if (cb.checked) list.push({ url: cb.dataset.dlUrl, title: map[cb.dataset.dlUrl] || '' }); }); if (!list.length) { toast('请先勾选要下载的章节'); return; } if (list.length > 300) { if (!confirm('已勾选 ' + list.length + ' 章,全本下载耗时较长且 TXT 文件较大,确定继续吗?')) return; } selectMode = false; removeSelectToolbar(); dlRunning = true; btnDownloadAll.textContent = '下载中 0/' + list.length; btnDownloadAll.disabled = false; renderCatalog(true); send({ type: 'downloadAll', chapters: list }); } btnDownloadAll.addEventListener('click', function (ev) { ev.stopPropagation(); if (dlRunning) { if (btnDownloadAll.textContent.indexOf('停止中') !== -1) return; btnDownloadAll.textContent = '停止中...'; btnDownloadAll.disabled = true; toast('正在停止下载...'); send({ type: 'downloadPause' }); return; } if (selectMode) return; openDownloadModal(); }); var dlSelected = 'all'; function openDownloadModal() { var total = tocData && tocData.length ? tocData.length : (C.length || 0); var cur = 0; if (C.length && d >= 0) { var _cn = parseChapterNumber(C[d].title || ''); cur = (_cn !== null && _cn > 0) ? _cn : (d + 1); } if (dlModalSub) dlModalSub.textContent = '全书' + (total ? '共 ' + total + ' 章' : '') + (cur ? ',当前第 ' + cur + ' 章' : ''); if (dlSubAll) dlSubAll.textContent = total ? '共 ' + total + ' 章' : '全部章节'; if (dlSubFrom) dlSubFrom.textContent = cur ? '第 ' + cur + ' 章 ~ 末尾' : '当前章 ~ 末尾'; dlFrom.value = cur || 1; dlTo.value = total || 1; dlOptions.style.display = ''; dlFoot.style.display = ''; dlProgress.classList.remove('show'); downloadModal.classList.add('visible'); selectDlOption('all'); } function closeDownloadModal() { downloadModal.classList.remove('visible'); } function selectDlOption(k) { dlSelected = k; var opts = dlOptions.querySelectorAll('.dl-option'); for (var i = 0; i < opts.length; i++) opts[i].classList.toggle('selected', opts[i].dataset.dl === k); } function confirmDownload() { var list = null; var allList = (tocData && tocData.length >= 20) ? tocData : C.slice(); if (!allList || !allList.length) { toast('暂无章节可下载'); return; } if (dlSelected === 'all') { list = allList; } else if (dlSelected === 'from') { var idx0 = findLoadedByUrl(currentUrl()); if (idx0 < 0) idx0 = 0; list = allList.slice(idx0); } else if (dlSelected === 'custom') { var a = parseInt(dlFrom.value, 10) || 1; var b = parseInt(dlTo.value, 10) || a; if (a > b) { var t = a; a = b; b = t; } list = []; for (var i = 0; i < allList.length; i++) { var _n = parseChapterNumber(allList[i].title || ''); if (_n !== null && _n >= a && _n <= b) list.push(allList[i]); } if (!list.length) { toast('该章节范围内没有可下载章节'); return; } } else { closeDownloadModal(); enterSelectMode(); return; } if (!list || !list.length) { toast('暂无章节可下载'); return; } if (list.length > 300) { if (!confirm('已选 ' + list.length + ' 章,全本下载耗时较长且 TXT 文件较大,确定继续吗?')) return; } dlRunning = true; btnDownloadAll.textContent = '下载中 0/' + list.length; btnDownloadAll.disabled = false; dlOptions.style.display = 'none'; dlFoot.style.display = 'none'; dlProgressTitle.textContent = '正在下载 ' + list.length + ' 章…'; dlProgressBar.style.width = '0%'; dlProgressPct.textContent = '0 / ' + list.length; dlProgress.classList.add('show'); send({ type: 'downloadAll', chapters: list }); } function updateDownloadProgress(cur, total) { if (dlProgress.classList.contains('show')) { dlProgressBar.style.width = (total > 0 ? (cur / total * 100) : 0).toFixed(1) + '%'; dlProgressPct.textContent = cur + ' / ' + total; dlProgressTitle.textContent = '正在下载 ' + cur + '/' + total + ' 章…'; } } dlOptions.addEventListener('click', function (ev) { var opt = ev.target.closest ? ev.target.closest('.dl-option') : null; if (opt && opt.dataset && opt.dataset.dl) selectDlOption(opt.dataset.dl); }); dlCancel.addEventListener('click', function (ev) { ev.stopPropagation(); closeDownloadModal(); }); dlConfirm.addEventListener('click', function (ev) { ev.stopPropagation(); confirmDownload(); }); dlStop.addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'downloadPause' }); toast('正在停止下载...'); }); downloadModal.addEventListener('click', function (ev) { if (ev.target === downloadModal) closeDownloadModal(); }); btnNight.addEventListener('click', function (ev) { ev.stopPropagation(); if (s.theme === 'dark') { s.theme = s.prevTheme || 'warm'; } else { s.prevTheme = s.theme; s.theme = 'dark'; } applyThemeAttr(); syncNightBtn(); syncThemeCards(); saveSettings(); toast(s.theme === 'dark' ? '已切换夜读模式' : '已切换日间模式'); }); function openSettings() { G = true; Y.classList.add('visible'); j.classList.add('visible'); hideBars(); } function closeSettings() { G = false; Y.classList.remove('visible'); j.classList.remove('visible'); } document.getElementById('btnSettings').addEventListener('click', function (ev) { ev.stopPropagation(); openSettings(); }); j.addEventListener('click', function () { closeSettings(); }); document.getElementById('btnManagePurify').addEventListener('click', function (ev) { ev.stopPropagation(); closeSettings(); openExtraPanel('purifyPanel'); }); document.getElementById('purifyClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); document.getElementById('purifyAddBtn').addEventListener('click', function (ev) { ev.stopPropagation(); var inp = document.getElementById('purifyInput'); var v2 = (inp.value || '').trim(); if (!v2) return; send({ type: 'addPurifyWords', words: v2 }); inp.value = ''; }); document.getElementById('purifyInput').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.stopPropagation(); var v2 = (this.value || '').trim(); if (!v2) return; send({ type: 'addPurifyWords', words: v2 }); this.value = ''; } }); (function () { var help = document.getElementById('purifyHelp'); var head = document.getElementById('purifyHelpHead'); if (!help || !head) return; head.addEventListener('click', function (ev) { ev.stopPropagation(); help.classList.toggle('open'); }); })(); purifyExportBtn.addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'exportPurify' }); }); purifyImportBtn.addEventListener('click', function (ev) { ev.stopPropagation(); try { var fi = document.createElement('input'); fi.type = 'file'; fi.accept = '.txt,.json,.csv'; fi.style.display = 'none'; fi.addEventListener('change', function () { var file = fi.files && fi.files[0]; if (!file) return; var rd = new FileReader(); rd.onload = function () { var text = String(rd.result || ''); if (!text.trim()) { toast('文件内容为空'); return; } send({ type: 'importPurifyText', text: text }); }; rd.onerror = function () { toast('文件读取失败'); }; rd.readAsText(file, 'utf-8'); fi.parentNode && fi.parentNode.removeChild(fi); }); document.body.appendChild(fi); fi.click(); } catch (err) { toast('无法打开文件选择器'); } }); var THEME_CSS = { warm: 'parchment', white: 'paper', green: 'green', dark: 'dark' }; var moonSVG = ''; var sunSVG = ''; function applyThemeAttr() { var _t = (s.theme === 'paper') ? 'white' : s.theme; document.documentElement.setAttribute('data-theme', THEME_CSS[_t] || 'parchment'); document.body.className = 'theme-' + _t; } function applyFont() { fontVal.textContent = s.fontSize; fontRange.value = s.fontSize; document.documentElement.style.setProperty('--body', s.fontSize + 'px'); document.documentElement.style.setProperty('--reader-fs', s.fontSize + 'px'); document.documentElement.style.setProperty('--reader-ts', (s.fontSize + 4) + 'px'); var cs = f.querySelectorAll('.chapter-content'); for (var i = 0; i < cs.length; i++) cs[i].style.fontSize = ''; var ts = f.querySelectorAll('.chapter-title'); for (var k = 0; k < ts.length; k++) ts[k].style.fontSize = ''; } function applyFontChoice() { var fstack = (s.font === 'sans') ? '-apple-system,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif' : '"Noto Serif SC","Source Han Serif SC","Songti SC","SimSun",serif'; document.documentElement.style.setProperty('--serif', fstack); } function syncThemeCards() { var cards = themeRow ? themeRow.querySelectorAll('.theme-card') : []; for (var i = 0; i < cards.length; i++) { cards[i].classList.toggle('on', cards[i].dataset.theme === s.theme || (s.theme === 'paper' && cards[i].dataset.theme === 'white')); } } function syncNightBtn() { if (s.theme === 'dark') { nightLabel.textContent = STR.day; btnNight.classList.add('on'); if (nightIcon) nightIcon.innerHTML = sunSVG; } else { nightLabel.textContent = STR.night; btnNight.classList.remove('on'); if (nightIcon) nightIcon.innerHTML = moonSVG; } } function applySettings() { applyThemeAttr(); applyFont(); document.documentElement.style.setProperty('--lh', s.lineHeight); var btns = lineSeg ? lineSeg.querySelectorAll('button') : []; for (var i = 0; i < btns.length; i++) btns[i].classList.toggle('on', parseFloat(btns[i].dataset.lh) === s.lineHeight); var fb = fontSegEl ? fontSegEl.querySelectorAll('button') : []; for (var k = 0; k < fb.length; k++) fb[k].classList.toggle('on', fb[k].dataset.font === (s.font || 'serif')); applyFontChoice(); syncThemeCards(); syncNightBtn(); syncConvSeg(); var cs = f.querySelectorAll('.chapter-content'); for (var j2 = 0; j2 < cs.length; j2++) cs[j2].style.lineHeight = ''; } function saveSettings() { send({ type: 'saveSettings', settings: s }); } fontRange.addEventListener('input', function (ev) { ev.stopPropagation(); s.fontSize = parseInt(fontRange.value, 10) || 18; applyFont(); saveSettings(); }); lineSeg.addEventListener('click', function (ev) { var tgt = ev.target; if (!tgt.dataset || !tgt.dataset.lh) return; ev.stopPropagation(); var btns = this.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) btns[i].classList.remove('on'); tgt.classList.add('on'); s.lineHeight = parseFloat(tgt.dataset.lh); document.documentElement.style.setProperty('--lh', s.lineHeight); saveSettings(); }); // 繁简设置段(B10) var convSegEl = document.getElementById('convSeg'); function syncConvSeg() { if (!convSegEl) return; var mode = s.simplified || 'off'; var btns = convSegEl.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) btns[i].classList.toggle('on', btns[i].dataset.conv === mode); } if (convSegEl) { syncConvSeg(); convSegEl.addEventListener('click', function (ev) { var tgt = ev.target; if (!tgt.dataset || !tgt.dataset.conv) return; ev.stopPropagation(); var btns = this.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) btns[i].classList.remove('on'); tgt.classList.add('on'); s.simplified = tgt.dataset.conv; saveSettings(); send({ type: 'setConvertMode', mode: s.simplified }); var names = { off: '原样', t2s: '繁→简', s2t: '简→繁' }; toast('已切换「' + names[s.simplified] + '」,正在重新加载当前章节…'); }); } themeRow.addEventListener('click', function (ev) { var card = ev.target.closest ? ev.target.closest('.theme-card') : null; if (!card || !card.dataset || !card.dataset.theme) return; ev.stopPropagation(); s.theme = card.dataset.theme; applyThemeAttr(); syncThemeCards(); if (s.theme !== 'dark') s.prevTheme = s.theme; syncNightBtn(); saveSettings(); var names = { warm: '羊皮纸', dark: '夜读', green: '护眼', white: '纸白', paper: '纸白' }; toast('已切换「' + (names[s.theme] || s.theme) + '」主题'); }); if (fontSegEl) fontSegEl.addEventListener('click', function (ev) { var tgt = ev.target; if (!tgt.dataset || !tgt.dataset.font) return; ev.stopPropagation(); if (tgt.classList.contains('on') && s.font === tgt.dataset.font) { s.font = 'serif'; var btns2 = this.querySelectorAll('button'); for (var j2 = 0; j2 < btns2.length; j2++) btns2[j2].classList.toggle('on', btns2[j2].dataset.font === 'serif'); applyFontChoice(); saveSettings(); toast('已恢复默认字体(衬线)'); return; } var btns = this.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) btns[i].classList.remove('on'); tgt.classList.add('on'); s.font = tgt.dataset.font; applyFontChoice(); saveSettings(); }); function closeExtraPanel() { extraVisible = false; whitelistPanel.classList.remove('visible'); historyPanel.classList.remove('visible'); var pp = document.getElementById('purifyPanel'); if (pp) pp.classList.remove('visible'); extraMask.style.display = 'none'; } function openExtraPanel(panelId) { closeExtraPanel(); extraVisible = true; var panel = document.getElementById(panelId); if (!panel) return; panel.classList.add('visible'); extraMask.style.display = 'block'; if (panelId === 'whitelistPanel') { send({ type: 'getWhitelist' }); } else if (panelId === 'historyPanel') { send({ type: 'getHistory' }); } else if (panelId === 'purifyPanel') { send({ type: 'getPurify' }); } setTimeout(function () { var list = panel.querySelector('.panel-list'); if (list) setupFade(list); }, 50); } function bindExtraToggle(btnId, panelId) { document.getElementById(btnId).addEventListener('click', function (ev) { ev.stopPropagation(); var panel = document.getElementById(panelId); if (extraVisible && panel.classList.contains('visible')) { closeExtraPanel(); return; } openExtraPanel(panelId); }); } document.getElementById('whitelistClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); document.getElementById('historyClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); extraMask.addEventListener('click', function () { closeExtraPanel(); }); bindExtraToggle('btnWhitelist', 'whitelistPanel'); bindExtraToggle('btnHistory', 'historyPanel'); function setupFade(list) { var wrap = list.parentNode; if (!wrap) return; var topMask = wrap.querySelector('.fade-mask.top'); var bottomMask = wrap.querySelector('.fade-mask.bottom'); if (!topMask || !bottomMask) return; function checkFade() { var scrollTop = list.scrollTop; var maxScroll = list.scrollHeight - list.clientHeight; topMask.classList.toggle('hidden', scrollTop <= 2); bottomMask.classList.toggle('hidden', maxScroll - scrollTop <= 2); } list.addEventListener('scroll', checkFade); setTimeout(checkFade, 100); var observer = new MutationObserver(function () { checkFade(); }); observer.observe(list, { childList: true, subtree: false }); list._fadeObserver = observer; list._fadeCleanup = function () { observer.disconnect(); list.removeEventListener('scroll', checkFade); }; } function normHost(d) { var s2 = String(d || '').trim().toLowerCase().replace(/^https?:\/\//i, '').split('/')[0].split('#')[0].split('?')[0]; var idx = s2.indexOf(':'); if (idx !== -1) s2 = s2.slice(0, idx); return s2; } function renderWhitelist(list) { var container = document.getElementById('whitelistList'); if (!container) return; container.innerHTML = ''; var arr = list || []; if (!arr.length) { container.innerHTML = '
暂无白名单
可在下方输入域名添加
'; return; } arr.forEach(function (it) { var isCur = normHost(it.domain) === normHost(currentHost); var item = document.createElement('div'); item.className = 'list-item' + (isCur ? ' hi' : ''); var main = document.createElement('div'); main.className = 'li-main'; main.addEventListener('click', function () { try { send({ type: 'navigateTo', url: 'https://' + it.domain }); } catch (e) {} }); var t = document.createElement('div'); t.className = 'li-title'; t.textContent = it.title || it.domain || '未知站点'; if (isCur) { var pill = document.createElement('span'); pill.className = 'tag-pill'; pill.textContent = '当前网站'; t.appendChild(pill); } var s2 = document.createElement('div'); s2.className = 'li-sub num'; s2.textContent = it.domain || ''; main.appendChild(t); main.appendChild(s2); var act = document.createElement('div'); act.className = 'li-action'; var del = document.createElement('button'); del.className = 'li-del'; del.setAttribute('aria-label', '删除'); del.innerHTML = ''; del.addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'removeWhitelist', domain: it.domain }); }); act.appendChild(del); item.appendChild(main); item.appendChild(act); container.appendChild(item); if (isCur) setTimeout(function () { try { item.scrollIntoView({ block: 'center' }); } catch (e) {} }, 120); }); } whitelistAddBtn.addEventListener('click', function (ev) { ev.stopPropagation(); var v2 = (whitelistInput.value || '').trim().toLowerCase(); if (!v2) return; send({ type: 'addWhitelist', domain: v2 }); whitelistInput.value = ''; }); whitelistInput.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.stopPropagation(); var v2 = (this.value || '').trim().toLowerCase(); if (!v2) return; send({ type: 'addWhitelist', domain: v2 }); this.value = ''; } }); function renderHistory(list) { var container = document.getElementById('historyList'); if (!container) return; container.innerHTML = ''; var sorted = (list || []).slice().sort(function (a, b) { return b.timestamp - a.timestamp; }); if (!sorted.length) { container.innerHTML = '
暂无历史记录
'; return; } var today = new Date(); today.setHours(0, 0, 0, 0); var yesterday = new Date(today.getTime() - 86400000); var groups = []; var curGroup = null; function groupLabel(ts) { var dd = new Date(ts); var st = new Date(dd); st.setHours(0, 0, 0, 0); if (st.getTime() === today.getTime()) return '今天'; if (st.getTime() === yesterday.getTime()) return '昨天'; return (dd.getMonth() + 1) + '月' + dd.getDate() + '日'; } sorted.forEach(function (entry) { var lb = groupLabel(entry.timestamp || Date.now()); if (!curGroup || curGroup.label !== lb) { curGroup = { label: lb, items: [] }; groups.push(curGroup); } curGroup.items.push(entry); }); groups.forEach(function (grp) { var gd = document.createElement('div'); gd.className = 'date-group'; gd.textContent = grp.label; container.appendChild(gd); grp.items.forEach(function (entry) { var entryKey = entry.bookKey || getBookRootKey(entry.url); var isCur = entryKey === currentBookKey; var item = document.createElement('div'); item.className = 'list-item' + (isCur ? ' hi' : ''); var main = document.createElement('div'); main.className = 'li-main'; main.addEventListener('click', function () { send({ type: 'navigateTo', url: entry.url }); }); var t = document.createElement('div'); t.className = 'li-title'; t.textContent = entry.title || '未命名'; if (isCur) { var pill = document.createElement('span'); pill.className = 'tag-pill'; pill.textContent = '在读'; t.appendChild(pill); } var s2 = document.createElement('div'); s2.className = 'li-sub num'; var hh = new Date(entry.timestamp || Date.now()); var hhmm = ('0' + hh.getHours()).slice(-2) + ':' + ('0' + hh.getMinutes()).slice(-2); var pstr = ''; if (entry.chapterTotal > 0 && entry.chapterNo > 0) pstr = ' · 第 ' + entry.chapterNo + ' / ' + entry.chapterTotal + ' 章'; else if (entry.chapterNo > 0) pstr = ' · 第 ' + entry.chapterNo + ' 章'; s2.textContent = hhmm + ' 阅读' + pstr + (isCur ? ' · 点击续读' : ''); main.appendChild(t); main.appendChild(s2); var act = document.createElement('div'); act.className = 'li-action'; var del = document.createElement('button'); del.className = 'li-del'; del.setAttribute('aria-label', '删除'); del.innerHTML = ''; del.addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'removeHistory', url: entry.url }); }); act.appendChild(del); item.appendChild(main); item.appendChild(act); container.appendChild(item); if (isCur) setTimeout(function () { try { item.scrollIntoView({ block: 'center' }); } catch (e) {} }, 120); }); }); } function renderPurify(list) { var container = document.getElementById('purifyList'); if (!container) return; container.innerHTML = ''; var arr = list || []; if (!arr.length) { container.innerHTML = '
暂无净化词
'; } else { arr.forEach(function (w) { var item = document.createElement('div'); item.className = 'word-item'; var span = document.createElement('span'); span.className = 'word-text'; span.textContent = w; var del = document.createElement('button'); del.className = 'word-del'; del.setAttribute('aria-label', '删除'); del.textContent = '✕'; del.addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'removePurifyWord', word: w }); }); item.appendChild(span); item.appendChild(del); container.appendChild(item); }); } var pt = document.getElementById('purifyTitle'); if (pt) pt.innerHTML = '净化词(' + arr.length + ')· 点✕删除'; } send({ type: 'ready' }); } var READER_JS = "(function () {\n'use strict';\n" + SHARED_SRC + "\n" + iframeMain.toString() + "\niframeMain();\n})();"; var READER_HTML = '' + '' + '阅读模式' + '' + READER_BODY + '