Universal Novel Reader

小说阅读脚本,统一阅读样式,内容去广告、修正拼音字、段落整理,自动下一页

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name               Universal Novel Reader
// @name:zh-CN         通用小说阅读
// @name:zh-TW         通用小說閱讀
// @namespace          https://github.com/Evan-acg/NovelReader
// @version            1.0.0
// @author             EvanstonLaw
// @description        小说阅读脚本,统一阅读样式,内容去广告、修正拼音字、段落整理,自动下一页
// @description:zh-CN  小说阅读脚本,统一阅读样式,内容去广告、修正拼音字、段落整理,自动下一页
// @description:zh-TW  小說閱讀腳本,統一閱讀樣式,內容去廣告、修正拼音字、段落整理,自動下一頁
// @license            GPL version 3
// @match              *://*/*
// @connect            github.com
// @connect            raw.githubusercontent.com
// @grant              GM_addStyle
// @grant              GM_getValue
// @grant              GM_info
// @grant              GM_openInTab
// @grant              GM_setValue
// @grant              GM_xmlhttpRequest
// @grant              unsafeWindow
// ==/UserScript==

(function () {
  'use strict';

  let debugEnabled = false;
  const logger = {
    info(...args) {
      console.log("[NovelReader]", ...args);
    },
    debug(...args) {
      if (debugEnabled) {
        console.log("[NovelReader DEBUG]", ...args);
      }
    },
    warn(...args) {
      console.warn("[NovelReader]", ...args);
    },
    error(...args) {
      console.error("[NovelReader]", ...args);
    },
    setDebug(enabled) {
      debugEnabled = enabled;
    }
  };
  function gmGetValue(key, defaultValue = "") {
    return (typeof GM_getValue === "function" ? GM_getValue(key, defaultValue) : defaultValue) ?? defaultValue;
  }
  function gmSetValue(key, value) {
    if (typeof GM_setValue === "function") {
      GM_setValue(key, value);
    }
  }
  async function gmFetch(url, timeout = 1e4) {
    if (typeof GM_xmlhttpRequest !== "function") {
      throw new Error("GM_xmlhttpRequest 不可用");
    }
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        url,
        method: "GET",
        timeout,
        onload: (response) => {
          if (response.status === 200) {
            resolve(response.responseText);
          } else {
            reject(new Error(`HTTP ${response.status}: ${url}`));
          }
        },
        onerror: (err) => reject(err),
        ontimeout: () => reject(new Error(`请求超时: ${url}`))
      });
    });
  }
  function gmOpenInTab(url) {
    if (typeof GM_openInTab === "function") {
      GM_openInTab(url);
    } else {
      window.open(url, "_blank");
    }
  }
  function gmAddStyle(css) {
    if (typeof GM_addStyle === "function") {
      GM_addStyle(css);
    } else {
      const style = document.createElement("style");
      style.textContent = css;
      document.head.appendChild(style);
    }
  }
  const RULE_URLS = {
    site: "https://raw.githubusercontent.com/Evan-acg/NovelReader/rules/site/site-rules.json",
    text: "https://raw.githubusercontent.com/Evan-acg/NovelReader/rules/replace/text-rules.json",
    s2t: "https://raw.githubusercontent.com/Evan-acg/NovelReader/rules/s2t/s2t-rules.json"
  };
  const SITE_RULES_URL = RULE_URLS.site;
  const TEXT_RULES_URL = RULE_URLS.text;
  const S2T_RULES_URL = RULE_URLS.s2t;
  const KEYS = {
    siteRulesUrl: "siteRulesUrl",
    siteRulesCache: "siteRulesCache",
    siteRulesCacheUpdatedAt: "siteRulesCacheUpdatedAt",
    textRulesUrl: "textRulesUrl",
    textRulesCache: "textRulesCache",
    textRulesCacheUpdatedAt: "textRulesCacheUpdatedAt",
    s2tRulesUrl: "s2tRulesUrl",
    s2tRulesCache: "s2tRulesCache",
    s2tRulesCacheUpdatedAt: "s2tRulesCacheUpdatedAt",
    customSiteRules: "customSiteRules",
    customReplaceRules: "customReplaceRules",
    enabledTextRuleGroups: "enabledTextRuleGroups",
    convertToTraditional: "convertToTraditional",
    splitContent: "splitContent",
    hideSidebar: "hideSidebar",
    hideHistoryMenu: "hideHistoryMenu",
    hideFooterNav: "hideFooterNav",
    hidePreferencesButton: "hidePreferencesButton",
    remainHeight: "remainHeight",
    maxRetries: "maxRetries",
    retryDelay: "retryDelay",
    imagePreload: "imagePreload",
    fontFamily: "fontFamily",
    fontSize: "fontSize",
    lineHeight: "lineHeight",
    contentWidth: "contentWidth",
    extraCss: "extraCss",
    keybindings: "keybindings",
    skinName: "skinName",
    disableAutoLaunch: "disableAutoLaunch",
    booklinkEnable: "booklinkEnable",
    language: "language",
    copyCurrentTitle: "copyCurrentTitle",
    addNextPageToHistory: "addNextPageToHistory",
    doubleClickPause: "doubleClickPause",
    scrollAnimate: "scrollAnimate",
    debug: "debug"
  };
  const DEFAULT_SETTINGS = {
    siteRulesUrl: SITE_RULES_URL,
    siteRulesCache: "",
    siteRulesCacheUpdatedAt: "",
    textRulesUrl: TEXT_RULES_URL,
    textRulesCache: "",
    textRulesCacheUpdatedAt: "",
    s2tRulesUrl: S2T_RULES_URL,
    s2tRulesCache: "",
    s2tRulesCacheUpdatedAt: "",
    customSiteRules: "[]",
    customReplaceRules: "[]",
    enabledTextRuleGroups: [],
    convertToTraditional: false,
    splitContent: false,
    hideSidebar: false,
    hideHistoryMenu: false,
    hideFooterNav: false,
    hidePreferencesButton: false,
    remainHeight: 300,
    maxRetries: 2,
    retryDelay: 2e3,
    imagePreload: true,
    fontFamily: '-apple-system, "Microsoft YaHei", "PingFang SC", sans-serif',
    fontSize: 18,
    lineHeight: 1.8,
    contentWidth: 800,
    extraCss: "",
    keybindings: {},
    skinName: "default",
    disableAutoLaunch: false,
    booklinkEnable: true,
    language: "zh-CN",
    copyCurrentTitle: false,
    addNextPageToHistory: true,
    doubleClickPause: true,
    scrollAnimate: true,
    debug: false
  };
  function getSetting(key, defaultValue = "") {
    try {
      return gmGetValue(key, defaultValue);
    } catch (e) {
      logger.warn(`读取设置 ${key} 失败:`, e);
      return defaultValue;
    }
  }
  function setSetting(key, value) {
    try {
      gmSetValue(key, value);
    } catch (e) {
      logger.warn(`保存设置 ${key} 失败:`, e);
    }
  }
  function getJsonSetting(key, defaultValue) {
    try {
      const raw = gmGetValue(key, "");
      if (!raw) return defaultValue;
      return JSON.parse(raw);
    } catch {
      return defaultValue;
    }
  }
  function setJsonSetting(key, value) {
    try {
      gmSetValue(key, JSON.stringify(value));
    } catch (e) {
      logger.warn(`保存 JSON 设置 ${key} 失败:`, e);
    }
  }
  function loadAllSettings() {
    const result = {};
    for (const [key, defaultValue] of Object.entries(DEFAULT_SETTINGS)) {
      const raw = getSetting(key, "");
      if (raw === "") {
        result[key] = defaultValue;
        continue;
      }
      const type = typeof defaultValue;
      if (type === "boolean") {
        result[key] = raw === "true";
      } else if (type === "number") {
        const n = Number(raw);
        result[key] = Number.isFinite(n) ? n : defaultValue;
      } else if (type === "object") {
        try {
          result[key] = JSON.parse(raw);
        } catch {
          result[key] = defaultValue;
        }
      } else {
        result[key] = raw;
      }
    }
    return result;
  }
  function saveSetting(key, value) {
    const dv = DEFAULT_SETTINGS[key];
    const type = typeof dv;
    let stored;
    if (type === "boolean") {
      stored = value ? "true" : "false";
    } else if (type === "number") {
      stored = String(value);
    } else if (type === "object") {
      stored = JSON.stringify(value);
    } else {
      stored = value;
    }
    setSetting(key, stored);
  }
  function success(value) {
    return { success: true, value };
  }
  function failure(error) {
    return { success: false, error };
  }
  const MAX_GROUPS = 50;
  const MAX_RULES_PER_GROUP = 500;
  const ALLOWED_FLAGS = /^[gimsuy]*$/;
  function validateTextRule(rule2) {
    if (!rule2 || typeof rule2 !== "object") {
      return failure(new Error("文本规则必须是对象"));
    }
    const r = rule2;
    if (typeof r.pattern !== "string" || !r.pattern.trim()) {
      return failure(new Error("缺少 pattern"));
    }
    if (typeof r.replacement !== "string") {
      return failure(new Error("缺少 replacement"));
    }
    if (r.flags !== void 0) {
      if (typeof r.flags !== "string" || !ALLOWED_FLAGS.test(r.flags)) {
        return failure(new Error(`不安全的 flags: ${String(r.flags)}`));
      }
    }
    try {
      new RegExp(r.pattern, r.flags || "");
    } catch {
      return failure(new Error(`无效的正则 pattern: ${String(r.pattern)}`));
    }
    return success({
      pattern: r.pattern,
      replacement: r.replacement,
      flags: r.flags
    });
  }
  function validateTextRuleGroup(group) {
    if (!group || typeof group !== "object") {
      return failure(new Error("规则组必须是对象"));
    }
    const g = group;
    if (typeof g.id !== "string" || !g.id.trim()) {
      return failure(new Error("规则组缺少 id"));
    }
    if (!Array.isArray(g.rules)) {
      return failure(new Error("规则组的 rules 必须是数组"));
    }
    const rules = g.rules;
    if (rules.length > MAX_RULES_PER_GROUP) {
      return failure(new Error(`规则组 "${String(g.id)}" 规则数量超过上限 ${MAX_RULES_PER_GROUP}`));
    }
    const validRules = [];
    for (let i = 0; i < rules.length; i++) {
      const result = validateTextRule(rules[i]);
      if (!result.success) {
        return failure(new Error(`规则组 "${String(g.id)}" 规则 ${i}: ${result.error.message}`));
      }
      validRules.push(result.value);
    }
    return success({
      id: g.id,
      name: typeof g.name === "string" ? g.name : g.id,
      enabledByDefault: g.enabledByDefault === true,
      rules: validRules
    });
  }
  function validateTextRuleSet(json) {
    if (!json || typeof json !== "object") {
      return failure(new Error("文本规则集必须是对象"));
    }
    const data = json;
    if (typeof data.version !== "number" || data.version < 1) {
      return failure(new Error("缺少或无效的 version"));
    }
    if (!Array.isArray(data.groups)) {
      return failure(new Error("groups 必须是数组"));
    }
    const groups2 = data.groups;
    if (groups2.length > MAX_GROUPS) {
      return failure(new Error(`规则组数量超过上限 ${MAX_GROUPS}`));
    }
    const validGroups = [];
    for (let i = 0; i < groups2.length; i++) {
      const result = validateTextRuleGroup(groups2[i]);
      if (!result.success) {
        return failure(new Error(`规则组 ${i}: ${result.error.message}`));
      }
      validGroups.push(result.value);
    }
    return success({
      version: data.version,
      updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
      groups: validGroups
    });
  }
  const MAX_RULES = 500;
  const SAFE_SELECTOR_REGEX = /^[a-zA-Z0-9\-_.#,:\[\]="'()\s*~+>|^$]+$/;
  const SAFE_URL_REGEX = /^[\x20-\x7E\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+$/;
  function hasSafeChars(value, regex) {
    if (!value) return true;
    return regex.test(value);
  }
  function validateSiteRule(rule2) {
    if (!rule2 || typeof rule2 !== "object") {
      return failure(new Error("规则必须是对象"));
    }
    const r = rule2;
    if (typeof r.id !== "string" || !r.id.trim()) {
      return failure(new Error("缺少 id"));
    }
    if (typeof r.url !== "string" || !r.url.trim()) {
      return failure(new Error(`规则 "${String(r.id)}" 缺少 url`));
    }
    const url = r.url;
    if (!hasSafeChars(url, SAFE_URL_REGEX)) {
      return failure(new Error(`规则 "${url}" 的 url 包含不安全字符`));
    }
    if (/^(javascript|data|vbscript):/i.test(url)) {
      return failure(new Error(`规则 "${url}" 的 url 不允许使用脚本协议`));
    }
    try {
      new RegExp(url);
    } catch {
      return failure(new Error(`规则 "${url}" 的 url 不是合法的正则表达式`));
    }
    const selectors = ["titleSelector", "bookTitleSelector", "chapterTitleSelector", "contentSelector", "prevSelector", "nextSelector", "indexSelector"];
    for (const key of selectors) {
      const value = r[key];
      if (value !== void 0 && (typeof value !== "string" || !hasSafeChars(value, SAFE_SELECTOR_REGEX))) {
        return failure(new Error(`规则 "${url}" 的 ${key} 包含不安全字符`));
      }
    }
    if (r.contentReplaceRules !== void 0) {
      if (!Array.isArray(r.contentReplaceRules)) {
        return failure(new Error(`规则 "${url}" 的 contentReplaceRules 必须是数组`));
      }
      const replaceRules = r.contentReplaceRules;
      for (let i = 0; i < replaceRules.length; i++) {
        const result = validateTextRule(replaceRules[i]);
        if (!result.success) {
          return failure(new Error(`规则 "${url}" 的 contentReplaceRules[${i}]: ${result.error.message}`));
        }
      }
    }
    return success(r);
  }
  function validateSiteRuleSet(json) {
    if (!json || typeof json !== "object") {
      return failure(new Error("规则集必须是一个对象"));
    }
    const data = json;
    if (typeof data.version !== "number" || data.version < 1) {
      return failure(new Error("缺少或无效的 version"));
    }
    if (!Array.isArray(data.rules)) {
      return failure(new Error("rules 必须是数组"));
    }
    const rules = data.rules;
    if (rules.length > MAX_RULES) {
      return failure(new Error(`规则数量超过上限 ${MAX_RULES}`));
    }
    const validRules = [];
    for (let i = 0; i < rules.length; i++) {
      const result = validateSiteRule(rules[i]);
      if (!result.success) {
        return failure(new Error(`规则索引 ${i}: ${result.error.message}`));
      }
      validRules.push(result.value);
    }
    return success({
      version: data.version,
      updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
      rules: validRules
    });
  }
  async function fetchRemoteSiteRules(url) {
    const targetUrl = url || SITE_RULES_URL;
    try {
      logger.info(`正在加载远程站点规则: ${targetUrl}`);
      const text = await gmFetch(targetUrl);
      let json;
      try {
        json = JSON.parse(text);
      } catch {
        return failure(new Error("远程站点规则 JSON 解析失败"));
      }
      const result = validateSiteRuleSet(json);
      if (!result.success) {
        return failure(new Error(`远程站点规则校验失败: ${result.error.message}`));
      }
      setJsonSetting(KEYS.siteRulesCache, result.value);
      setJsonSetting(KEYS.siteRulesCacheUpdatedAt, Date.now());
      logger.info(`站点规则加载成功,共 ${result.value.rules.length} 条`);
      return result;
    } catch (e) {
      logger.warn("远程站点规则拉取失败:", e);
      return failure(e instanceof Error ? e : new Error(String(e)));
    }
  }
  function loadCachedSiteRules() {
    const cached = getJsonSetting(KEYS.siteRulesCache, null);
    if (!cached || !cached.version || !Array.isArray(cached.rules)) {
      return failure(new Error("无可用缓存"));
    }
    logger.info(`使用缓存的站点规则,共 ${cached.rules.length} 条`);
    return success(cached);
  }
  async function loadSiteRulesWithFallback(url) {
    const remoteResult = await fetchRemoteSiteRules(url);
    if (remoteResult.success) {
      return remoteResult.value;
    }
    const cachedResult = loadCachedSiteRules();
    if (cachedResult.success) {
      return cachedResult.value;
    }
    return { version: 1, updatedAt: "", rules: [] };
  }
  function loadCustomSiteRules() {
    const raw = getJsonSetting(KEYS.customSiteRules, []);
    if (!Array.isArray(raw)) return [];
    const valid = [];
    for (const item of raw) {
      const result = validateSiteRule(item);
      if (result.success) {
        valid.push(result.value);
      } else {
        logger.warn("自定义站点规则校验失败:", result.error.message);
      }
    }
    return valid;
  }
  let compiledRules = [];
  let initialized$1 = false;
  async function initRuleRegistry(url) {
    if (initialized$1) return;
    const ruleSet = await loadSiteRulesWithFallback(url);
    const customRules = loadCustomSiteRules();
    const items = [];
    for (const rule2 of customRules) {
      try {
        items.push({
          rule: rule2,
          regex: new RegExp(rule2.url, "i"),
          priority: rule2.priority ?? 900,
          source: "custom"
        });
      } catch {
        logger.warn(`自定义规则正则无效: ${rule2.url}`);
      }
    }
    for (const rule2 of ruleSet.rules) {
      if (customRules.some((cr) => cr.id === rule2.id)) continue;
      try {
        items.push({
          rule: rule2,
          regex: new RegExp(rule2.url, "i"),
          priority: rule2.priority ?? 500,
          source: "remote"
        });
      } catch {
        logger.warn(`规则正则无效: ${rule2.url}`);
      }
    }
    items.sort((a, b) => b.priority - a.priority);
    compiledRules = items;
    initialized$1 = true;
    logger.info(`规则注册完成,共 ${items.length} 条`);
  }
  function matchRule(url) {
    const target = url;
    for (const item of compiledRules) {
      if (item.regex.test(target)) {
        if (item.rule.excludeUrl) {
          try {
            if (new RegExp(item.rule.excludeUrl, "i").test(target)) {
              continue;
            }
          } catch {
          }
        }
        logger.info(`规则匹配: ${item.rule.name} (${item.rule.id}) [${item.source}]`);
        return item.rule;
      }
    }
    return null;
  }
  async function fetchRemoteTextRules(url) {
    const targetUrl = url || TEXT_RULES_URL;
    try {
      logger.info(`正在加载远程文本规则: ${targetUrl}`);
      const text = await gmFetch(targetUrl);
      let json;
      try {
        json = JSON.parse(text);
      } catch {
        return failure(new Error("远程文本规则 JSON 解析失败"));
      }
      const result = validateTextRuleSet(json);
      if (!result.success) {
        return failure(new Error(`远程文本规则校验失败: ${result.error.message}`));
      }
      setJsonSetting(KEYS.textRulesCache, result.value);
      setJsonSetting(KEYS.textRulesCacheUpdatedAt, Date.now());
      logger.info(`文本规则加载成功,共 ${result.value.groups.length} 组`);
      return result;
    } catch (e) {
      logger.warn("远程文本规则拉取失败:", e);
      return failure(e instanceof Error ? e : new Error(String(e)));
    }
  }
  function loadCachedTextRules() {
    const cached = getJsonSetting(KEYS.textRulesCache, null);
    if (!cached || !cached.version || !Array.isArray(cached.groups)) {
      return failure(new Error("无可用缓存"));
    }
    logger.info(`使用缓存的文本规则,共 ${cached.groups.length} 组`);
    return success(cached);
  }
  async function loadTextRulesWithFallback(url) {
    const remoteResult = await fetchRemoteTextRules(url);
    if (remoteResult.success) {
      return remoteResult.value;
    }
    const cachedResult = loadCachedTextRules();
    if (cachedResult.success) {
      return cachedResult.value;
    }
    return { version: 1, updatedAt: "", groups: [] };
  }
  function loadCustomReplaceRules() {
    const raw = getJsonSetting(KEYS.customReplaceRules, []);
    if (!Array.isArray(raw)) return [];
    const valid = [];
    for (const item of raw) {
      const result = validateTextRule(item);
      if (result.success) {
        valid.push(result.value);
      } else {
        logger.warn("自定义文本规则校验失败:", result.error.message);
      }
    }
    return valid;
  }
  let combinedRules = [];
  let groups = [];
  let initialized = false;
  async function initTextRuleRegistry(url) {
    if (initialized) return;
    const ruleSet = await loadTextRulesWithFallback(url);
    const customRules = loadCustomReplaceRules();
    const enabledGroups = getJsonSetting(KEYS.enabledTextRuleGroups, []);
    groups = ruleSet.groups;
    const merged = [];
    for (const group of groups) {
      if (enabledGroups.length > 0 && !enabledGroups.includes(group.id)) {
        continue;
      }
      if (enabledGroups.length === 0 && !group.enabledByDefault) {
        continue;
      }
      merged.push(...group.rules);
    }
    merged.push(...customRules);
    combinedRules = merged;
    initialized = true;
    logger.info(`文本规则注册完成,共 ${merged.length} 条`);
  }
  function getCombinedTextRules() {
    return combinedRules;
  }
  function getTextContent(el) {
    var _a;
    return ((_a = el == null ? void 0 : el.textContent) == null ? void 0 : _a.trim()) ?? "";
  }
  function querySelector(parent, selector) {
    return parent.querySelector(selector);
  }
  function querySelectorAll(parent, selector) {
    return Array.from(parent.querySelectorAll(selector));
  }
  function removeElements(selector, root = document) {
    querySelectorAll(root, selector).forEach((el) => el.remove());
  }
  function getAbsoluteUrl(href, baseUrl) {
    if (!href) return "";
    try {
      return new URL(href, baseUrl).href;
    } catch {
      return "";
    }
  }
  let placeholderId = 0;
  const PLACEHOLDER_PREFIX = "\0NOVEL_READER_PROTECT_";
  function protectHtmlTags(html) {
    const map = /* @__PURE__ */ new Map();
    let result = html;
    result = result.replace(/<img\b[^>]*\/?>/gi, (match) => {
      const placeholder = `${PLACEHOLDER_PREFIX}${placeholderId++}_`;
      map.set(placeholder, match);
      return placeholder;
    });
    result = result.replace(/<a\b[^>]*>[\s\S]*?<\/a>/gi, (match) => {
      const placeholder = `${PLACEHOLDER_PREFIX}${placeholderId++}_`;
      map.set(placeholder, match);
      return placeholder;
    });
    return { protectedHtml: result, map };
  }
  function restoreHtmlTags(html, map) {
    let result = html;
    for (const [placeholder, original] of map) {
      while (result.includes(placeholder)) {
        result = result.replace(placeholder, original);
      }
    }
    return result;
  }
  function removeUnwantedElements(html) {
    let result = html;
    result = result.replace(/<script\b[\s\S]*?<\/script>/gi, "");
    result = result.replace(/<iframe\b[\s\S]*?<\/iframe>/gi, "");
    result = result.replace(/<style\b[\s\S]*?<\/style>/gi, "");
    result = result.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, "");
    return result;
  }
  function applyTextRules(html, rules) {
    let result = html;
    for (const rule2 of rules) {
      try {
        const regex = new RegExp(rule2.pattern, rule2.flags || "g");
        result = result.replace(regex, rule2.replacement);
      } catch (e) {
        logger.warn(`文本规则执行失败: pattern="${rule2.pattern}"`, e);
      }
    }
    return result;
  }
  function convertBrToParagraphs(html) {
    let result = html.trim();
    result = result.replace(/^(<br\s*\/?>\s*)+/i, "");
    result = result.replace(/(<br\s*\/?>\s*)+$/i, "");
    result = result.replace(/(?:<br\s*\/?>\s*){2,}/gi, "</p><p>");
    if (!/^\s*<p\b/i.test(result)) {
      result = "<p>" + result;
    }
    if (!/<\/p>\s*$/i.test(result)) {
      result = result + "</p>";
    }
    return result;
  }
  function removeEmptyParagraphs(html) {
    return html.replace(/<p\b[^>]*>\s*(<br\s*\/?>\s*)*\s*<\/p>/gi, "");
  }
  function forceSplitContent(html) {
    if (!html || /<(p|br|div)\b/i.test(html)) {
      return html;
    }
    return html.replace(/([。!?;!?;])\s*/g, "$1</p><p>").replace(/^/, "<p>").replace(/$/, "</p>");
  }
  async function loadS2TMapping() {
    const url = getSetting(KEYS.s2tRulesUrl, S2T_RULES_URL);
    try {
      logger.info(`正在加载简繁映射: ${url}`);
      const text = await gmFetch(url);
      const json = JSON.parse(text);
      if (typeof json === "object" && json !== null && !Array.isArray(json)) {
        setJsonSetting(KEYS.s2tRulesCache, json);
        setJsonSetting(KEYS.s2tRulesCacheUpdatedAt, Date.now());
        logger.info("简繁映射加载成功");
        return json;
      }
    } catch (e) {
      logger.warn("远程简繁映射拉取失败:", e);
    }
    const cached = getJsonSetting(KEYS.s2tRulesCache, null);
    if (cached && typeof cached === "object" && !Array.isArray(cached)) {
      logger.info("使用缓存的简繁映射");
      return cached;
    }
    logger.warn("无可用简繁映射,简繁转换将不生效");
    return {};
  }
  function convertS2T(text, mapping) {
    let result = "";
    for (const char of text) {
      result += mapping[char] || char;
    }
    return result;
  }
  function cleanContent(rawHtml, textRules2, options = {}) {
    if (!rawHtml) {
      return { html: "", text: "" };
    }
    let html = rawHtml;
    html = removeUnwantedElements(html);
    const { protectedHtml, map } = protectHtmlTags(html);
    html = protectedHtml;
    html = applyTextRules(html, textRules2);
    html = restoreHtmlTags(html, map);
    if (options.splitContent) {
      html = forceSplitContent(html);
    }
    html = convertBrToParagraphs(html);
    html = removeEmptyParagraphs(html);
    if (options.convertToTraditional && options.s2tMapping) {
      html = convertS2T(html, options.s2tMapping);
    }
    const text = html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
    return { html, text };
  }
  const VIP_MIN_TEXT_LENGTH = 50;
  const CANDIDATE_CONTENT_SELECTORS = [
    "#content",
    ".content",
    "#article",
    ".article",
    "#chapter-content",
    ".chapter-content",
    "#read-content",
    "#booktxt",
    "#BookText",
    "#novel-content",
    "#htmlContent",
    ".text",
    "#text",
    ".chapter",
    "#chapter",
    ".book-content",
    "#book-content",
    ".main-text",
    ".read-content",
    ".section-content",
    "#chaptercontent",
    ".showtxt",
    "#contents",
    ".article-content",
    "#main-content",
    ".post-content",
    ".entry-content"
  ];
  const TITLE_SEPARATORS = /[_\-\|–—·]\s*/;
  function extractBookTitleAuto(doc, rule2) {
    for (const selector of ["h1", "h2", "h3", ".title", "#title", ".book-title"]) {
      const el = querySelector(doc, selector);
      if (el) {
        const text = getTextContent(el);
        if (text && !text.includes("章") && !text.includes("节") && text.length < 50) {
          return text;
        }
      }
    }
    return parseDocTitle(doc).bookTitle;
  }
  function extractChapterTitleAuto(doc, rule2) {
    for (const selector of ["h1", "h2", "h3", ".title", "#title", ".chapter-title", ".chapterTitle"]) {
      const el = querySelector(doc, selector);
      if (el) {
        const text = getTextContent(el);
        if (text && (text.includes("章") || text.includes("节") || text.includes("第"))) {
          return text;
        }
      }
    }
    return parseDocTitle(doc).chapterTitle;
  }
  function parseDocTitle(doc) {
    const title = doc.title.trim();
    if (!title) return { bookTitle: "", chapterTitle: "" };
    const parts = title.split(TITLE_SEPARATORS).map((s) => s.trim()).filter(Boolean);
    if (parts.length === 1) {
      return { bookTitle: parts[0], chapterTitle: parts[0] };
    }
    let bookPart = "";
    let chapterPart = "";
    for (const part of parts) {
      if (/[第序]/.test(part) && /[章节回篇]/.test(part)) {
        chapterPart = chapterPart || part;
      } else if (chapterPart) {
        break;
      }
    }
    if (!chapterPart) {
      bookPart = parts[0];
      chapterPart = parts[parts.length - 1];
    } else {
      bookPart = parts.find((p) => p !== chapterPart && p.length > 1) || parts[0];
    }
    return { bookTitle: bookPart, chapterTitle: chapterPart };
  }
  function extractTitle(doc, rule2) {
    let bookTitle = "";
    let chapterTitle = "";
    if (rule2.bookTitleSelector) {
      const el = querySelector(doc, rule2.bookTitleSelector);
      bookTitle = getTextContent(el);
    }
    if (rule2.chapterTitleSelector) {
      const el = querySelector(doc, rule2.chapterTitleSelector);
      chapterTitle = getTextContent(el);
    }
    if (bookTitle && chapterTitle) {
      return { bookTitle, chapterTitle };
    }
    if (rule2.titleSelector) {
      const el = querySelector(doc, rule2.titleSelector);
      const text = getTextContent(el);
      if (text) {
        const parts = text.split(TITLE_SEPARATORS).map((s) => s.trim()).filter(Boolean);
        if (parts.length >= 2) {
          return { bookTitle: bookTitle || parts[0], chapterTitle: chapterTitle || parts[1] };
        }
        if (!bookTitle) bookTitle = text;
        if (!chapterTitle) chapterTitle = text;
      }
    }
    if (rule2.bookTitleRegex || rule2.chapterTitleRegex) {
      const docTitle = doc.title.trim();
      if (rule2.bookTitleRegex) {
        try {
          const m = docTitle.match(new RegExp(rule2.bookTitleRegex, "i"));
          if (m && m[1]) bookTitle = bookTitle || m[1].trim();
        } catch {
        }
      }
      if (rule2.chapterTitleRegex) {
        try {
          const m = docTitle.match(new RegExp(rule2.chapterTitleRegex, "i"));
          if (m && m[1]) chapterTitle = chapterTitle || m[1].trim();
        } catch {
        }
      }
      if (bookTitle && chapterTitle) {
        return { bookTitle, chapterTitle };
      }
    }
    if (!bookTitle) bookTitle = extractBookTitleAuto(doc);
    if (!chapterTitle) chapterTitle = extractChapterTitleAuto(doc);
    return { bookTitle, chapterTitle };
  }
  function extractContent(doc, rule2, removeSelectors) {
    var _a;
    let contentEl = null;
    if (rule2.contentSelector) {
      contentEl = querySelector(doc, rule2.contentSelector);
    }
    if (!contentEl) {
      let bestEl = null;
      let bestScore = 0;
      for (const selector of CANDIDATE_CONTENT_SELECTORS) {
        const el = querySelector(doc, selector);
        if (!el) continue;
        const text = ((_a = el.textContent) == null ? void 0 : _a.trim()) ?? "";
        if (text.length < VIP_MIN_TEXT_LENGTH) continue;
        const pCount = el.querySelectorAll("p, div > br").length;
        const score = text.length + pCount * 200;
        if (score > bestScore) {
          bestScore = score;
          bestEl = el;
        }
      }
      contentEl = bestEl;
    }
    if (!contentEl) {
      return { html: "", text: "" };
    }
    const clone = contentEl.cloneNode(true);
    if (removeSelectors) {
      for (const selector of removeSelectors) {
        removeElements(selector, clone);
      }
    }
    return {
      html: clone.innerHTML,
      text: (clone.textContent ?? "").trim()
    };
  }
  const NEXT_PATTERNS = ["下一章", "下一页", "下一节", "后一章", "→", "&#8594;", "next"];
  const PREV_PATTERNS = ["上一章", "上一页", "上一节", "前一章", "←", "&#8592;", "prev"];
  const INDEX_PATTERNS = ["目录", "返回目录", "作品目录", "章节目录", "索引", "index"];
  function findNavLink(doc, patterns, relValue) {
    const links = querySelectorAll(doc, "a");
    for (const link of links) {
      if (relValue && (link.rel === relValue || link.getAttribute("rel") === relValue)) {
        return { url: link.href, text: getTextContent(link) };
      }
      const text = getTextContent(link);
      if (text && patterns.some((p) => text.includes(p))) {
        return { url: link.href, text: getTextContent(link) };
      }
    }
    return null;
  }
  function extractNavigation(doc, rule2, currentUrl) {
    let prevUrl;
    let nextUrl;
    let indexUrl;
    if (rule2.prevSelector) {
      const el = querySelector(doc, rule2.prevSelector);
      prevUrl = (el == null ? void 0 : el.href) || void 0;
    }
    if (rule2.nextSelector) {
      const el = querySelector(doc, rule2.nextSelector);
      nextUrl = (el == null ? void 0 : el.href) || void 0;
    }
    if (rule2.indexSelector) {
      const el = querySelector(doc, rule2.indexSelector);
      indexUrl = (el == null ? void 0 : el.href) || void 0;
    }
    if (!prevUrl) {
      const found = findNavLink(doc, PREV_PATTERNS, "prev");
      prevUrl = (found == null ? void 0 : found.url) || void 0;
    }
    if (!nextUrl) {
      const found = findNavLink(doc, NEXT_PATTERNS, "next");
      nextUrl = (found == null ? void 0 : found.url) || void 0;
    }
    if (!indexUrl) {
      const found = findNavLink(doc, INDEX_PATTERNS);
      indexUrl = (found == null ? void 0 : found.url) || void 0;
    }
    const toAbs = (href) => {
      if (!href) return void 0;
      const abs = getAbsoluteUrl(href, currentUrl);
      if (!abs || abs.startsWith("javascript:") || abs === currentUrl) return void 0;
      return abs;
    };
    return {
      prevUrl: toAbs(prevUrl),
      nextUrl: toAbs(nextUrl),
      indexUrl: toAbs(indexUrl)
    };
  }
  function detectVip(contentText, rule2) {
    if (rule2.isVip) return true;
    if (!contentText || contentText.length < VIP_MIN_TEXT_LENGTH) return true;
    return false;
  }
  function parseChapter(doc, currentUrl, rule2, textRules2, cleanOptions2) {
    logger.info(`解析章节: ${currentUrl}`);
    let { bookTitle, chapterTitle } = extractTitle(doc, rule2);
    const { html } = extractContent(doc, rule2, rule2.removeSelectors);
    const nav = extractNavigation(doc, rule2, currentUrl);
    const allTextRules = [
      ...rule2.contentReplaceRules ?? [],
      ...textRules2 ?? []
    ];
    const cleaned = cleanContent(html, allTextRules, cleanOptions2);
    if ((cleanOptions2 == null ? void 0 : cleanOptions2.convertToTraditional) && (cleanOptions2 == null ? void 0 : cleanOptions2.s2tMapping)) {
      bookTitle = convertS2T(bookTitle, cleanOptions2.s2tMapping);
      chapterTitle = convertS2T(chapterTitle, cleanOptions2.s2tMapping);
    }
    const isVip = detectVip(cleaned.text, rule2);
    const isSection = cleaned.text.length < 20 || /^第[一二三四五六七八九十百零]+卷/.test(chapterTitle || "");
    const result = {
      url: currentUrl,
      bookTitle: bookTitle || rule2.name || "",
      chapterTitle: chapterTitle || "",
      documentTitle: doc.title,
      contentHtml: cleaned.html,
      contentText: cleaned.text,
      prevUrl: nav.prevUrl,
      nextUrl: nav.nextUrl,
      indexUrl: nav.indexUrl,
      isVip,
      isSection
    };
    logger.info(`解析完成: ${result.bookTitle} - ${result.chapterTitle}`);
    return result;
  }
  const SKIN_PRESETS = {
    default: {
      name: "缺省皮肤",
      css: ""
    },
    dark: {
      name: "暗色皮肤",
      css: `
.nr-reader-container { color: #666; background-color: rgba(0,0,0,.1); }
`
    },
    white: {
      name: "白底黑字",
      css: `
.nr-reader-container { color: black; background-color: white; }
.nr-chapter-title { font-weight: bold; border-bottom-color: currentColor; }
`
    },
    night: {
      name: "夜间模式",
      css: `
.nr-reader-container { color: #939392; background: #2d2d2d; }
.nr-settings-btn { background: white; }
.nr-chapter-body img { background-color: #c0c0c0; }
.nr-sidebar-item.active { color: #939392; }
`
    },
    "night-1": {
      name: "夜间模式1",
      css: `
.nr-reader-container { color: #679; background-color: black; }
.nr-settings-btn { background-color: white !important; }
.nr-chapter-title { color: #3399FF; background-color: #121212; }
`
    },
    "night-2": {
      name: "夜间模式2",
      css: `
.nr-reader-container { color: #AAAAAA; background-color: #121212; }
.nr-settings-btn { background-color: white; }
.nr-chapter-body img { background-color: #c0c0c0; }
.nr-chapter-title { color: #3399FF; background-color: #121212; }
.nr-reader-container a { color: #E0BC2D; }
.nr-reader-container a:visited { color: #AAAAAA; }
.nr-reader-container a:hover { color: #3399FF; }
.nr-reader-container a:active { color: #423F3F; }
`
    },
    "night-duokan": {
      name: "夜间模式(多看)",
      css: `
.nr-reader-container { color: #4A4A4A; background: #101819; }
.nr-settings-btn { background: white; }
.nr-chapter-body img { background-color: #c0c0c0; }
`
    },
    orange: {
      name: "橙色背景",
      css: `
.nr-reader-container { color: #24272c; background-color: #FEF0E1; }
`
    },
    green: {
      name: "绿色背景",
      css: `
.nr-reader-container { color: black; background-color: #d8e2c8; }
`
    },
    "green-2": {
      name: "绿色背景2",
      css: `
.nr-reader-container { color: black; background-color: #CCE8CF; }
`
    },
    blue: {
      name: "蓝色背景",
      css: `
.nr-reader-container { color: black; background-color: #E7F4FE; }
`
    },
    brown: {
      name: "棕黄背景",
      css: `
.nr-reader-container { color: black; background-color: #C2A886; }
`
    },
    classic: {
      name: "经典皮肤",
      css: `
.nr-reader-container { color: black; background-color: #EAEAEE; }
.nr-chapter-title { background-color: #f0f0f0; }
`
    },
    "qidian-parchment-dark": {
      name: "起点牛皮纸(深色)",
      css: `
.nr-reader-container { color: black; background: url("http://qidian.gtimg.com/qd/images/read.qidian.com/theme/body_theme1_bg_2x.0.3.png"); }
`
    },
    "qidian-parchment-light": {
      name: "起点牛皮纸(浅色)",
      css: `
.nr-reader-container { color: black; background: url("http://qidian.gtimg.com/qd/images/read.qidian.com/theme/theme_1_bg_2x.0.3.png"); }
`
    },
    "qidian-black": {
      name: "起点黑色",
      css: `
.nr-reader-container,
.nr-sidebar,
.nr-sidebar-header { color: #666; background: #111 url("https://qidian.gtimg.com/qd/images/read.qidian.com/theme/theme_6_bg.45ad3.png") repeat; }
.nr-settings-btn { background: white; }
`
    },
    "green-bright": {
      name: "绿色亮字",
      css: `
.nr-reader-container,
.nr-sidebar,
.nr-sidebar-header,
.nr-sidebar-item.active { color: rgb(187,215,188); background-color: rgb(18,44,20); }
`
    }
  };
  const READER_CSS = `
.nr-reader-container {
  position: fixed;
  inset: 0;
  z-index: 2147483647;
  background: #f5f5f5;
  color: #333;
  font-family: var(--nr-font-family);
  font-size: var(--nr-font-size);
  line-height: var(--nr-line-height);
  display: flex;
  overflow: hidden;
}
.nr-sidebar {
  order: 0;
  position: relative;
  width: 220px;
  min-width: 220px;
  height: 100%;
  background: inherit;
  border-right: 1px solid rgba(0,0,0,0.14);
  display: flex;
  flex-direction: column;
  overflow: visible;
  transition: margin-left 0.25s ease, width 0.25s ease;
}
.nr-sidebar::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255,255,255,0.08);
  pointer-events: none;
}
.nr-sidebar-hidden .nr-sidebar {
  margin-left: -220px;
}
.nr-sidebar-header {
  position: relative;
  z-index: 1;
  padding: 12px 14px;
  font-weight: 700;
  font-size: 15px;
  border-bottom: 1px solid rgba(0,0,0,0.12);
  flex-shrink: 0;
}
.nr-sidebar-list {
  position: relative;
  z-index: 1;
  flex: 1;
  overflow-y: auto;
  padding: 6px 0;
}
.nr-sidebar-item {
  display: block;
  padding: 7px 14px;
  font-size: 14px;
  color: #555;
  text-decoration: none;
  border-left: 3px solid transparent;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.nr-sidebar-item:hover {
  background: #f0f0f0;
  color: #222;
}
.nr-sidebar-item.active {
  border-left-color: #4a90d9;
  background: #e8f0fe;
  color: #1a73e8;
  font-weight: 600;
}
.nr-sidebar-toggle {
  position: absolute;
  right: -24px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2147483648;
  width: 24px;
  height: 60px;
  background: #ddd;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
  font-size: 14px;
  color: #666;
  padding: 0;
}
.nr-sidebar-toggle:hover {
  background: #ccc;
}
.nr-content-area {
  order: 1;
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 40px 80px 80px;
  max-width: var(--nr-content-max-width);
  margin: 0 auto;
  scroll-behavior: smooth;
  text-align: left;
}
.nr-chapter {
  margin-bottom: 48px;
}
.nr-chapter-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 24px;
  padding-bottom: 12px;
  border-bottom: 2px solid #4a90d9;
  color: #222;
}
.nr-chapter-body {
  word-break: break-word;
  text-align: left;
}
.nr-chapter-body p {
  margin: 0 0 1em;
  text-indent: 2em;
}
.nr-chapter-body img {
  display: block;
  margin: 1em auto;
  max-width: 100%;
  height: auto;
}
.nr-bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 44px;
  background: #fff;
  border-top: 1px solid #ddd;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 32px;
  z-index: 2147483647;
  transition: transform 0.25s ease, opacity 0.25s ease;
}
.nr-nav-hidden .nr-bottom-nav {
  transform: translateY(100%);
  opacity: 0;
}
.nr-bottom-nav a {
  font-size: 15px;
  color: #4a90d9;
  text-decoration: none;
  padding: 6px 16px;
}
.nr-bottom-nav a:hover {
  background: #f0f0f0;
  border-radius: 4px;
}
.nr-settings-btn {
  position: fixed;
  top: 12px;
  right: 12px;
  z-index: 2147483648;
  width: 32px;
  height: 32px;
  border: none;
  background: rgba(0,0,0,0.06);
  border-radius: 50%;
  cursor: pointer;
  font-size: 16px;
  line-height: 32px;
  text-align: center;
  color: #666;
  transition: opacity 0.25s ease;
}
.nr-settings-btn:hover {
  background: rgba(0,0,0,0.12);
}
.nr-quiet .nr-sidebar,
.nr-quiet .nr-settings-btn,
.nr-quiet .nr-sidebar-toggle {
  opacity: 0;
  pointer-events: none;
}
.nr-quiet .nr-bottom-nav {
  transform: translateY(100%);
  opacity: 0;
}
.nr-loading-indicator {
  text-align: center;
  padding: 20px;
  color: #999;
  font-size: 14px;
}
.nr-error-indicator {
  text-align: center;
  padding: 20px;
  color: #c00;
  font-size: 14px;
}
.nr-error-indicator a {
  color: #4a90d9;
  cursor: pointer;
}
@media (max-width: 768px) {
  .nr-sidebar {
    width: 100%;
    position: fixed;
    inset: 0;
    z-index: 2147483648;
  }
  .nr-sidebar-hidden .nr-sidebar {
    margin-left: -100%;
  }
  .nr-content-area {
    padding: 20px 16px 60px;
  }
  .nr-chapter-body p {
    text-indent: 2em;
  }
  .nr-bottom-nav {
    gap: 12px;
  }
}
`;
  let extraCssEl = null;
  let skinCssEl = null;
  function injectReaderStyles() {
    gmAddStyle(READER_CSS);
  }
  function updateReaderStyleVars(settings) {
    const container = document.querySelector(".nr-reader-container");
    if (!container) return;
    container.style.setProperty("--nr-font-family", settings.fontFamily);
    container.style.setProperty("--nr-font-size", settings.fontSize + "px");
    container.style.setProperty("--nr-line-height", String(settings.lineHeight));
    container.style.setProperty("--nr-content-max-width", settings.contentWidth + "px");
  }
  function updateSkinCss(skinName) {
    var _a;
    const css = ((_a = SKIN_PRESETS[skinName]) == null ? void 0 : _a.css) ?? "";
    if (!skinCssEl) {
      skinCssEl = document.createElement("style");
      skinCssEl.id = "nr-skin-css";
      document.head.appendChild(skinCssEl);
    }
    skinCssEl.textContent = css;
  }
  function updateExtraCss(css) {
    if (!extraCssEl) {
      extraCssEl = document.createElement("style");
      extraCssEl.id = "nr-extra-css";
      document.head.appendChild(extraCssEl);
    }
    extraCssEl.textContent = css;
  }
  let sidebarRef = null;
  function createSidebar(container, chapters, activeIndex, onChapterClick) {
    var _a;
    const sidebar = document.createElement("div");
    sidebar.className = "nr-sidebar";
    const header = document.createElement("div");
    header.className = "nr-sidebar-header";
    header.textContent = ((_a = chapters[0]) == null ? void 0 : _a.bookTitle) || "章节目录";
    const list = document.createElement("div");
    list.className = "nr-sidebar-list";
    for (let i = 0; i < chapters.length; i++) {
      const item = createSidebarItem(chapters[i], i, i === activeIndex, onChapterClick);
      list.appendChild(item);
    }
    sidebar.appendChild(header);
    sidebar.appendChild(list);
    container.appendChild(sidebar);
    const toggle = document.createElement("button");
    toggle.className = "nr-sidebar-toggle";
    toggle.textContent = "◀";
    toggle.addEventListener("click", () => {
      if (container.classList.contains("nr-sidebar-hidden")) {
        showSidebar();
      } else {
        hideSidebar();
      }
    });
    sidebar.appendChild(toggle);
    sidebarRef = { el: sidebar, listEl: list, toggleEl: toggle };
    return sidebarRef;
  }
  function createSidebarItem(chapter, index, isActive, onChapterClick) {
    const item = document.createElement("a");
    item.className = "nr-sidebar-item" + (isActive ? " active" : "");
    item.textContent = chapter.chapterTitle || `第 ${index + 1} 章`;
    item.addEventListener("click", (e) => {
      if (e.button === 1 || e.ctrlKey || e.metaKey) {
        if (chapter.url) {
          e.preventDefault();
          window.open(chapter.url, "_blank");
          return;
        }
      }
      e.preventDefault();
      onChapterClick(index);
    });
    return item;
  }
  function addSidebarItem(chapter, onChapterClick) {
    if (!sidebarRef) return;
    const items = sidebarRef.listEl.querySelectorAll(".nr-sidebar-item");
    const index = items.length;
    const item = createSidebarItem(chapter, index, false, onChapterClick);
    sidebarRef.listEl.appendChild(item);
  }
  function setActiveSidebarItem(index) {
    if (!sidebarRef) return;
    const items = sidebarRef.listEl.querySelectorAll(".nr-sidebar-item");
    items.forEach((item, i) => {
      if (i === index) {
        item.classList.add("active");
      } else {
        item.classList.remove("active");
      }
    });
  }
  function showSidebar() {
    const container = document.querySelector(".nr-reader-container");
    if (container) {
      container.classList.remove("nr-sidebar-hidden");
    }
  }
  function hideSidebar() {
    const container = document.querySelector(".nr-reader-container");
    if (container) {
      container.classList.add("nr-sidebar-hidden");
    }
  }
  function toggleSidebarVisibility() {
    const container = document.querySelector(".nr-reader-container");
    if (!container) return;
    if (container.classList.contains("nr-sidebar-hidden")) {
      showSidebar();
    } else {
      hideSidebar();
    }
  }
  let navEl = null;
  function createBottomNav(container, links, onNavigate) {
    navEl = document.createElement("div");
    navEl.className = "nr-bottom-nav";
    const prev = document.createElement("a");
    prev.textContent = "上一章";
    prev.href = links.prevUrl || "#";
    prev.style.visibility = links.prevUrl ? "" : "hidden";
    prev.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.prevUrl) onNavigate(links.prevUrl);
    });
    const index = document.createElement("a");
    index.textContent = "目录";
    index.href = links.indexUrl || "#";
    index.style.visibility = links.indexUrl ? "" : "hidden";
    index.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.indexUrl) window.open(links.indexUrl, "_top");
    });
    const next = document.createElement("a");
    next.textContent = "下一章";
    next.href = links.nextUrl || "#";
    next.style.visibility = links.nextUrl ? "" : "hidden";
    next.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.nextUrl) onNavigate(links.nextUrl);
    });
    navEl.appendChild(prev);
    navEl.appendChild(index);
    navEl.appendChild(next);
    container.appendChild(navEl);
    return navEl;
  }
  function updateBottomNav(links, onNavigate) {
    if (!navEl) return;
    navEl.innerHTML = "";
    const prev = document.createElement("a");
    prev.textContent = "上一章";
    prev.href = links.prevUrl || "#";
    prev.style.visibility = links.prevUrl ? "" : "hidden";
    prev.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.prevUrl) onNavigate(links.prevUrl);
    });
    const index = document.createElement("a");
    index.textContent = "目录";
    index.href = links.indexUrl || "#";
    index.style.visibility = links.indexUrl ? "" : "hidden";
    index.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.indexUrl) window.open(links.indexUrl, "_top");
    });
    const next = document.createElement("a");
    next.textContent = "下一章";
    next.href = links.nextUrl || "#";
    next.style.visibility = links.nextUrl ? "" : "hidden";
    next.addEventListener("click", (e) => {
      e.preventDefault();
      if (links.nextUrl) onNavigate(links.nextUrl);
    });
    navEl.appendChild(prev);
    navEl.appendChild(index);
    navEl.appendChild(next);
  }
  const loadedUrls = /* @__PURE__ */ new Set();
  const failedUrls = /* @__PURE__ */ new Set();
  const loadingUrls = /* @__PURE__ */ new Set();
  function isUrlLoaded(url) {
    return loadedUrls.has(url);
  }
  function markUrlLoaded(url) {
    loadedUrls.add(url);
  }
  function clearLoadedUrls() {
    loadedUrls.clear();
  }
  function isUrlFailed(url) {
    return failedUrls.has(url);
  }
  function markUrlFailed(url) {
    failedUrls.add(url);
  }
  function isUrlLoading(url) {
    return loadingUrls.has(url);
  }
  function markUrlLoading(url) {
    loadingUrls.add(url);
  }
  async function loadNextChapter(url, rule2, textRules2, cleanOptions2, maxRetries = 2, retryDelay = 2e3) {
    if (isUrlLoaded(url)) {
      logger.info(`跳过已加载 URL: ${url}`);
      return { status: "skipped", chapter: null, url };
    }
    if (isUrlFailed(url)) {
      logger.info(`跳过已失败 URL: ${url}`);
      return { status: "skipped", chapter: null, url };
    }
    if (isUrlLoading(url)) {
      logger.info(`跳过正在加载中的 URL: ${url}`);
      return { status: "skipped", chapter: null, url };
    }
    markUrlLoading(url);
    try {
      let lastError;
      for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
          logger.info(`加载下一页${attempt > 0 ? ` (重试 ${attempt}/${maxRetries})` : ""}: ${url}`);
          const html = await gmFetch(url);
          const parser = new DOMParser();
          const doc = parser.parseFromString(html, "text/html");
          const chapter = parseChapter(doc, url, rule2, textRules2, cleanOptions2);
          markUrlLoaded(url);
          return { status: "loaded", chapter, url };
        } catch (e) {
          lastError = e;
          logger.warn(`加载下一页失败 (尝试 ${attempt + 1}/${maxRetries + 1}): ${url}`, e);
          if (attempt < maxRetries) {
            await new Promise((resolve) => setTimeout(resolve, retryDelay));
          }
        }
      }
      markUrlFailed(url);
      const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
      logger.warn(`加载下一页最终失败: ${url}`, lastError);
      return { status: "failed", chapter: null, url, error: errorMessage };
    } finally {
      loadingUrls.delete(url);
    }
  }
  function preloadImages(contentHtml, baseUrl) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(contentHtml, "text/html");
    const imgs = doc.querySelectorAll("img[src]");
    for (const img of imgs) {
      const src = img.getAttribute("src");
      if (src) {
        try {
          new Image().src = new URL(src, baseUrl).href;
        } catch {
          new Image().src = src;
        }
      }
    }
  }
  const PANEL_CSS = `
.nr-panel-overlay {
  position: fixed;
  inset: 0;
  z-index: 2147483649;
  background: rgba(0,0,0,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
}
.nr-panel {
  background: #fff;
  border-radius: 8px;
  width: 500px;
  max-width: 92vw;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.nr-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid #eee;
  flex-shrink: 0;
}
.nr-panel-header h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
}
.nr-panel-close-btn {
  width: 28px;
  height: 28px;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 20px;
  color: #999;
  border-radius: 4px;
  line-height: 1;
  padding: 0;
}
.nr-panel-close-btn:hover {
  background: #f0f0f0;
  color: #333;
}
.nr-panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px 20px;
}
.nr-panel-section {
  margin-bottom: 20px;
}
.nr-panel-section-title {
  font-size: 13px;
  font-weight: 700;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid #f0f0f0;
}
.nr-panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
  min-height: 32px;
}
.nr-panel-row label {
  font-size: 14px;
  color: #333;
  flex-shrink: 0;
  margin-right: 12px;
}
.nr-panel-row input[type="text"],
.nr-panel-row input[type="number"],
.nr-panel-row select {
  width: 200px;
  padding: 4px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 13px;
}
.nr-panel-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  margin: 0;
}
.nr-panel-row textarea {
  width: 100%;
  min-height: 80px;
  padding: 6px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 12px;
  font-family: monospace;
  resize: vertical;
}
.nr-panel-row-full {
  flex-direction: column;
  align-items: flex-start;
}
.nr-panel-row-full label {
  margin-bottom: 4px;
}
.nr-panel-save-hint {
  font-size: 11px;
  color: #aaa;
  text-align: center;
  margin-top: 12px;
}
`;
  let overlayEl = null;
  let onChangeCallback = null;
  let currentSettings = null;
  let stylesInjected = false;
  function ensureStyles() {
    if (!stylesInjected) {
      gmAddStyle(PANEL_CSS);
      stylesInjected = true;
    }
  }
  function createRow(label, control, fullWidth = false) {
    const row = document.createElement("div");
    row.className = fullWidth ? "nr-panel-row nr-panel-row-full" : "nr-panel-row";
    const lbl = document.createElement("label");
    lbl.textContent = label;
    row.appendChild(lbl);
    row.appendChild(control);
    return row;
  }
  function createTextInput(key, value) {
    const input = document.createElement("input");
    input.type = "text";
    input.value = value;
    input.addEventListener("change", () => {
      saveSetting(key, input.value);
      onChangeCallback == null ? void 0 : onChangeCallback(key, input.value);
    });
    return input;
  }
  function createNumberInput(key, value, step = 1) {
    const input = document.createElement("input");
    input.type = "number";
    input.value = String(value);
    if (step !== 1) input.step = String(step);
    input.addEventListener("change", () => {
      const num = Number(input.value);
      if (!isNaN(num)) {
        saveSetting(key, num);
        onChangeCallback == null ? void 0 : onChangeCallback(key, num);
      }
    });
    return input;
  }
  function createCheckbox(key, value) {
    const input = document.createElement("input");
    input.type = "checkbox";
    input.checked = value;
    input.addEventListener("change", () => {
      saveSetting(key, input.checked);
      onChangeCallback == null ? void 0 : onChangeCallback(key, input.checked);
    });
    return input;
  }
  function createTextarea(key, value) {
    const textarea = document.createElement("textarea");
    textarea.value = value;
    textarea.addEventListener("change", () => {
      saveSetting(key, textarea.value);
      onChangeCallback == null ? void 0 : onChangeCallback(key, textarea.value);
    });
    return textarea;
  }
  function createSkinSelect(value) {
    const select = document.createElement("select");
    for (const [key, preset] of Object.entries(SKIN_PRESETS)) {
      const option = document.createElement("option");
      option.value = key;
      option.textContent = preset.name;
      select.appendChild(option);
    }
    select.value = value;
    select.addEventListener("change", () => {
      saveSetting("skinName", select.value);
      onChangeCallback == null ? void 0 : onChangeCallback("skinName", select.value);
    });
    return select;
  }
  function buildPanel(settings) {
    const panel = document.createElement("div");
    panel.className = "nr-panel";
    const header = document.createElement("div");
    header.className = "nr-panel-header";
    const title = document.createElement("h3");
    title.textContent = "阅读设置";
    const closeBtn = document.createElement("button");
    closeBtn.className = "nr-panel-close-btn";
    closeBtn.textContent = "×";
    closeBtn.addEventListener("click", closePreferencesPanel);
    header.appendChild(title);
    header.appendChild(closeBtn);
    const body = document.createElement("div");
    body.className = "nr-panel-body";
    function addSection(sectionTitle) {
      const section = document.createElement("div");
      section.className = "nr-panel-section";
      const st = document.createElement("div");
      st.className = "nr-panel-section-title";
      st.textContent = sectionTitle;
      section.appendChild(st);
      body.appendChild(section);
      return section;
    }
    const styleSection = addSection("阅读样式");
    styleSection.appendChild(createRow("皮肤预设", createSkinSelect(settings.skinName)));
    styleSection.appendChild(createRow("字体", createTextInput("fontFamily", settings.fontFamily)));
    styleSection.appendChild(createRow("字号(px)", createNumberInput("fontSize", settings.fontSize)));
    styleSection.appendChild(createRow("行高", createNumberInput("lineHeight", settings.lineHeight, 0.1)));
    styleSection.appendChild(createRow("内容宽度(px)", createNumberInput("contentWidth", settings.contentWidth)));
    styleSection.appendChild(createRow("自定义CSS", createTextarea("extraCss", settings.extraCss), true));
    const toggleSection = addSection("功能开关");
    toggleSection.appendChild(createRow("简繁转换", createCheckbox("convertToTraditional", settings.convertToTraditional)));
    toggleSection.appendChild(createRow("强制分段", createCheckbox("splitContent", settings.splitContent)));
    toggleSection.appendChild(createRow("隐藏历史章节菜单", createCheckbox("hideHistoryMenu", settings.hideHistoryMenu)));
    toggleSection.appendChild(createRow("隐藏底部导航", createCheckbox("hideFooterNav", settings.hideFooterNav)));
    toggleSection.appendChild(createRow("隐藏设置按钮", createCheckbox("hidePreferencesButton", settings.hidePreferencesButton)));
    toggleSection.appendChild(createRow("图片预加载", createCheckbox("imagePreload", settings.imagePreload)));
    toggleSection.appendChild(createRow("调试日志", createCheckbox("debug", settings.debug)));
    toggleSection.appendChild(createRow("禁用自动启动", createCheckbox("disableAutoLaunch", settings.disableAutoLaunch)));
    toggleSection.appendChild(createRow("启用Booklink", createCheckbox("booklinkEnable", settings.booklinkEnable)));
    toggleSection.appendChild(createRow("下一页加入历史", createCheckbox("addNextPageToHistory", settings.addNextPageToHistory)));
    toggleSection.appendChild(createRow("双击暂停", createCheckbox("doubleClickPause", settings.doubleClickPause)));
    toggleSection.appendChild(createRow("滚动动画", createCheckbox("scrollAnimate", settings.scrollAnimate)));
    const urlSection = addSection("远程地址");
    urlSection.appendChild(createRow("站点规则", createTextInput("siteRulesUrl", settings.siteRulesUrl)));
    urlSection.appendChild(createRow("文本规则", createTextInput("textRulesUrl", settings.textRulesUrl)));
    urlSection.appendChild(createRow("简繁映射", createTextInput("s2tRulesUrl", settings.s2tRulesUrl)));
    const customSection = addSection("自定义规则");
    const groupInput = document.createElement("input");
    groupInput.type = "text";
    groupInput.value = settings.enabledTextRuleGroups.join(", ");
    groupInput.addEventListener("change", () => {
      const arr = groupInput.value.split(",").map((s) => s.trim()).filter(Boolean);
      saveSetting("enabledTextRuleGroups", arr);
      onChangeCallback == null ? void 0 : onChangeCallback("enabledTextRuleGroups", arr);
    });
    customSection.appendChild(createRow("启用规则组(逗号分隔)", groupInput));
    customSection.appendChild(createRow("自定义站点规则(JSON)", createTextarea("customSiteRules", settings.customSiteRules), true));
    customSection.appendChild(createRow("自定义替换规则(JSON)", createTextarea("customReplaceRules", settings.customReplaceRules), true));
    const loadSection = addSection("连续加载");
    loadSection.appendChild(createRow("触发距离(px)", createNumberInput("remainHeight", settings.remainHeight)));
    loadSection.appendChild(createRow("最大重试", createNumberInput("maxRetries", settings.maxRetries)));
    loadSection.appendChild(createRow("重试间隔(ms)", createNumberInput("retryDelay", settings.retryDelay)));
    const kbSection = addSection("快捷键");
    const kbTextarea = document.createElement("textarea");
    kbTextarea.value = JSON.stringify(settings.keybindings, null, 2);
    kbTextarea.addEventListener("change", () => {
      try {
        const parsed = JSON.parse(kbTextarea.value);
        saveSetting("keybindings", parsed);
        onChangeCallback == null ? void 0 : onChangeCallback("keybindings", parsed);
      } catch {
      }
    });
    kbSection.appendChild(createRow("快捷键配置(JSON)", kbTextarea, true));
    const hint = document.createElement("div");
    hint.className = "nr-panel-save-hint";
    hint.textContent = "修改后自动保存";
    body.appendChild(hint);
    panel.appendChild(header);
    panel.appendChild(body);
    return panel;
  }
  function openPreferencesPanel(onChange) {
    if (overlayEl) return;
    ensureStyles();
    currentSettings = loadAllSettings();
    onChangeCallback = onChange ?? null;
    const panel = buildPanel(currentSettings);
    overlayEl = document.createElement("div");
    overlayEl.className = "nr-panel-overlay";
    overlayEl.appendChild(panel);
    overlayEl.addEventListener("click", (e) => {
      if (e.target === overlayEl) {
        closePreferencesPanel();
      }
    });
    document.body.appendChild(overlayEl);
  }
  function closePreferencesPanel() {
    if (overlayEl) {
      overlayEl.remove();
      overlayEl = null;
      onChangeCallback = null;
      currentSettings = null;
    }
  }
  function isPanelOpen() {
    return overlayEl !== null;
  }
  const DEFAULT_KEYBINDINGS = {
    openIndex: "enter",
    prevChapter: "arrowleft",
    nextChapter: "arrowright",
    pageUp: ",",
    pageDown: ".",
    openSettings: "ctrl+,",
    toggleSidebar: "ctrl+b",
    toggleQuietMode: "ctrl+q"
  };
  let keydownHandler = null;
  let customBindings = {};
  function shouldIgnore() {
    var _a;
    const el = document.activeElement;
    if (!el) return false;
    const tag = el.tagName;
    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
    if (((_a = el.getAttribute) == null ? void 0 : _a.call(el, "contenteditable")) === "true") return true;
    return false;
  }
  function matchesBinding(e, binding) {
    const parts = binding.toLowerCase().split("+");
    const modifiers = parts.filter((p) => p !== parts[parts.length - 1]);
    const key = parts[parts.length - 1];
    const ctrlRequired = modifiers.includes("ctrl");
    const shiftRequired = modifiers.includes("shift");
    const altRequired = modifiers.includes("alt");
    if (e.ctrlKey !== ctrlRequired) return false;
    if (e.shiftKey !== shiftRequired) return false;
    if (e.altKey !== altRequired) return false;
    return e.key.toLowerCase() === key;
  }
  function getScrollContainer() {
    return document.querySelector(".nr-content-area");
  }
  function initKeyboard(handlers, keybindings) {
    customBindings = { ...DEFAULT_KEYBINDINGS, ...keybindings };
    keydownHandler = (e) => {
      var _a, _b, _c, _d, _e;
      if (shouldIgnore()) return;
      if (e.key === "Escape") {
        if (isPanelOpen()) {
          e.preventDefault();
          closePreferencesPanel();
          return;
        }
        return;
      }
      if (isPanelOpen()) return;
      if (matchesBinding(e, customBindings.openIndex)) {
        e.preventDefault();
        (_a = handlers.onOpenIndex) == null ? void 0 : _a.call(handlers);
        return;
      }
      if (matchesBinding(e, customBindings.prevChapter)) {
        e.preventDefault();
        (_b = handlers.onPrevChapter) == null ? void 0 : _b.call(handlers);
        return;
      }
      if (matchesBinding(e, customBindings.nextChapter)) {
        e.preventDefault();
        (_c = handlers.onNextChapter) == null ? void 0 : _c.call(handlers);
        return;
      }
      if (matchesBinding(e, customBindings.pageUp)) {
        e.preventDefault();
        const container = getScrollContainer();
        if (container) {
          container.scrollTop -= container.clientHeight * 0.8;
        }
        return;
      }
      if (matchesBinding(e, customBindings.pageDown)) {
        e.preventDefault();
        const container = getScrollContainer();
        if (container) {
          container.scrollTop += container.clientHeight * 0.8;
        }
        return;
      }
      if (matchesBinding(e, customBindings.openSettings)) {
        e.preventDefault();
        if (handlers.onOpenSettings) {
          handlers.onOpenSettings();
        } else {
          openPreferencesPanel();
        }
        return;
      }
      if (matchesBinding(e, customBindings.toggleSidebar)) {
        e.preventDefault();
        (_d = handlers.onToggleSidebar) == null ? void 0 : _d.call(handlers);
        return;
      }
      if (matchesBinding(e, customBindings.toggleQuietMode)) {
        e.preventDefault();
        (_e = handlers.onToggleQuietMode) == null ? void 0 : _e.call(handlers);
        return;
      }
    };
    document.addEventListener("keydown", keydownHandler);
    return () => {
      if (keydownHandler) {
        document.removeEventListener("keydown", keydownHandler);
        keydownHandler = null;
      }
    };
  }
  let state = null;
  let rule = null;
  let textRules = [];
  let cleanOptions = {};
  let containerEl = null;
  let contentAreaEl = null;
  let intersectionObserver = null;
  let isLoadingNext = false;
  let loadingIndicatorEl = null;
  let errorIndicatorEl = null;
  let scrollHandler = null;
  let keyboardCleanup = null;
  function ensureViewport() {
    if (!document.querySelector('meta[name="viewport"]')) {
      const meta = document.createElement("meta");
      meta.name = "viewport";
      meta.content = "width=device-width, initial-scale=1.0";
      document.head.appendChild(meta);
    }
  }
  function renderChapterElement(chapter, index) {
    const wrapper = document.createElement("div");
    wrapper.className = "nr-chapter";
    wrapper.setAttribute("data-chapter-index", String(index));
    const title = document.createElement("h2");
    title.className = "nr-chapter-title";
    title.textContent = chapter.chapterTitle || chapter.bookTitle || `第 ${index + 1} 章`;
    const body = document.createElement("div");
    body.className = "nr-chapter-body";
    body.innerHTML = chapter.contentHtml;
    wrapper.appendChild(title);
    wrapper.appendChild(body);
    return wrapper;
  }
  function setupIntersectionObserver() {
    if (!contentAreaEl) return;
    if (typeof IntersectionObserver === "undefined") {
      return;
    }
    if (intersectionObserver) {
      intersectionObserver.disconnect();
    }
    intersectionObserver = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            const index = Number(entry.target.getAttribute("data-chapter-index"));
            if (!isNaN(index) && state && index !== state.activeIndex) {
              state.activeIndex = index;
              setActiveSidebarItem(index);
              applyTitleUpdate(index);
              updateHistory(index);
            }
          }
        }
      },
      {
        root: contentAreaEl,
        threshold: 0.3
      }
    );
    const chapters = contentAreaEl.querySelectorAll(".nr-chapter");
    chapters.forEach((ch) => intersectionObserver.observe(ch));
  }
  function applyTitleUpdate(index) {
    if (!state || !state.chapters[index]) return;
    const chapter = state.chapters[index];
    const newTitle = chapter.bookTitle ? `${chapter.chapterTitle || ""} - ${chapter.bookTitle}` : chapter.chapterTitle;
    if (newTitle && document.title !== newTitle) {
      document.title = newTitle;
    }
  }
  function updateHistory(index) {
    if (!state || !state.chapters[index]) return;
    const settings = loadAllSettings();
    if (!settings.addNextPageToHistory) return;
    const chapter = state.chapters[index];
    const title = chapter.bookTitle ? `${chapter.chapterTitle || ""} - ${chapter.bookTitle}` : chapter.chapterTitle || "";
    let url;
    try {
      url = chapter.url && new URL(chapter.url).origin === location.origin ? chapter.url : void 0;
    } catch {
      url = void 0;
    }
    history.pushState(
      { chapterIndex: index, chapterTitle: chapter.chapterTitle, bookTitle: chapter.bookTitle },
      title,
      url
    );
  }
  function showLoadingIndicator() {
    if (!contentAreaEl || loadingIndicatorEl) return;
    loadingIndicatorEl = document.createElement("div");
    loadingIndicatorEl.className = "nr-loading-indicator";
    loadingIndicatorEl.textContent = "正在加载下一章...";
    contentAreaEl.appendChild(loadingIndicatorEl);
  }
  function hideLoadingIndicator() {
    if (loadingIndicatorEl) {
      loadingIndicatorEl.remove();
      loadingIndicatorEl = null;
    }
  }
  function showErrorIndicator(url) {
    if (!contentAreaEl || errorIndicatorEl) return;
    errorIndicatorEl = document.createElement("div");
    errorIndicatorEl.className = "nr-error-indicator";
    const link = document.createElement("a");
    link.textContent = "加载失败,点击手动打开下一页";
    link.href = url;
    link.target = "_blank";
    errorIndicatorEl.appendChild(link);
    contentAreaEl.appendChild(errorIndicatorEl);
  }
  function removeErrorIndicator() {
    if (errorIndicatorEl) {
      errorIndicatorEl.remove();
      errorIndicatorEl = null;
    }
  }
  async function triggerAutoLoad(url) {
    if (isLoadingNext || !rule) return;
    if (isUrlFailed(url)) return;
    isLoadingNext = true;
    removeErrorIndicator();
    showLoadingIndicator();
    const st = loadAllSettings();
    const maxRetries = st.maxRetries;
    const retryDelay = st.retryDelay;
    const result = await loadNextChapter(url, rule, textRules, cleanOptions, maxRetries, retryDelay);
    hideLoadingIndicator();
    if (result.status === "loaded" && result.chapter) {
      if (st.imagePreload) {
        preloadImages(result.chapter.contentHtml, url);
      }
      appendChapter(result.chapter);
    } else if (result.status === "failed") {
      showErrorIndicator(url);
    }
    isLoadingNext = false;
  }
  function setupScrollLoad() {
    if (!contentAreaEl) return;
    const st = loadAllSettings();
    scrollHandler = () => {
      if (!contentAreaEl || isLoadingNext || !state) return;
      if (state.autoLoadPaused) return;
      const lastChapter = state.chapters[state.chapters.length - 1];
      if (!(lastChapter == null ? void 0 : lastChapter.nextUrl)) return;
      const { scrollTop, scrollHeight, clientHeight } = contentAreaEl;
      if (scrollHeight - scrollTop - clientHeight < st.remainHeight) {
        triggerAutoLoad(lastChapter.nextUrl);
      }
    };
    contentAreaEl.addEventListener("scroll", scrollHandler, { passive: true });
  }
  function onSettingChange(key, value) {
    const all = loadAllSettings();
    switch (key) {
      case "fontFamily":
      case "fontSize":
      case "lineHeight":
      case "contentWidth":
        updateReaderStyleVars(all);
        break;
      case "hideSidebar":
      case "hideHistoryMenu":
        if (value) {
          containerEl == null ? void 0 : containerEl.classList.add("nr-sidebar-hidden");
        } else {
          containerEl == null ? void 0 : containerEl.classList.remove("nr-sidebar-hidden");
        }
        if (state) state.sidebarVisible = !value;
        break;
      case "hideFooterNav":
        if (value) {
          containerEl == null ? void 0 : containerEl.classList.add("nr-nav-hidden");
        } else {
          containerEl == null ? void 0 : containerEl.classList.remove("nr-nav-hidden");
        }
        break;
      case "hidePreferencesButton": {
        const btn = document.querySelector(".nr-settings-btn");
        if (btn) btn.style.display = value ? "none" : "";
        break;
      }
      case "extraCss":
        updateExtraCss(value);
        break;
      case "skinName":
        updateSkinCss(value);
        break;
      case "debug":
        logger.setDebug(value);
        break;
      case "keybindings": {
        if (keyboardCleanup) {
          keyboardCleanup();
          keyboardCleanup = null;
        }
        setupKeyboard();
        break;
      }
    }
  }
  function setupKeyboard() {
    const handlers = {
      onOpenIndex: () => {
        const last = state == null ? void 0 : state.chapters[state.chapters.length - 1];
        if (last == null ? void 0 : last.indexUrl) {
          window.open(last.indexUrl, "_blank");
        }
      },
      onPrevChapter: () => {
        var _a;
        if (state && state.activeIndex > 0) {
          scrollToChapter(state.activeIndex - 1);
        } else if ((_a = state == null ? void 0 : state.chapters[0]) == null ? void 0 : _a.prevUrl) {
          navigateToChapter(state.chapters[0].prevUrl);
        }
      },
      onNextChapter: () => {
        if (state && state.activeIndex < state.chapters.length - 1) {
          scrollToChapter(state.activeIndex + 1);
        } else {
          const last = state == null ? void 0 : state.chapters[state.chapters.length - 1];
          if (last == null ? void 0 : last.nextUrl) {
            navigateToChapter(last.nextUrl);
          }
        }
      },
      onToggleSidebar: () => toggleSidebarVisibility(),
      onToggleQuietMode: () => toggleQuietMode(),
      onOpenSettings: () => openPreferencesPanel(onSettingChange)
    };
    keyboardCleanup = initKeyboard(handlers, loadAllSettings().keybindings);
  }
  function renderReaderView(initialChapter, r, tr, co) {
    rule = r;
    textRules = tr;
    cleanOptions = co;
    const settings = loadAllSettings();
    injectReaderStyles();
    updateReaderStyleVars(settings);
    updateSkinCss(settings.skinName);
    if (settings.extraCss) {
      updateExtraCss(settings.extraCss);
    }
    ensureViewport();
    document.body.innerHTML = "";
    containerEl = document.createElement("div");
    containerEl.className = "nr-reader-container";
    if (settings.hideSidebar || settings.hideHistoryMenu) {
      containerEl.classList.add("nr-sidebar-hidden");
    }
    if (settings.hideFooterNav) {
      containerEl.classList.add("nr-nav-hidden");
    }
    contentAreaEl = document.createElement("div");
    contentAreaEl.className = "nr-content-area";
    const chapterEl = renderChapterElement(initialChapter, 0);
    contentAreaEl.appendChild(chapterEl);
    containerEl.appendChild(contentAreaEl);
    document.body.appendChild(containerEl);
    const settingsBtn = document.createElement("button");
    settingsBtn.className = "nr-settings-btn";
    settingsBtn.textContent = "⚙";
    settingsBtn.title = "设置";
    if (settings.hidePreferencesButton) {
      settingsBtn.style.display = "none";
    }
    settingsBtn.addEventListener("click", () => {
      openPreferencesPanel(onSettingChange);
    });
    document.body.appendChild(settingsBtn);
    state = {
      chapters: [initialChapter],
      activeIndex: 0,
      sidebarVisible: !(settings.hideSidebar || settings.hideHistoryMenu),
      quietMode: false,
      autoLoadPaused: false
    };
    createSidebar(containerEl, state.chapters, 0, (index) => {
      scrollToChapter(index);
    });
    createBottomNav(
      containerEl,
      {
        prevUrl: initialChapter.prevUrl,
        indexUrl: initialChapter.indexUrl,
        nextUrl: initialChapter.nextUrl
      },
      async (url) => {
        await navigateToChapter(url);
      }
    );
    setupIntersectionObserver();
    applyTitleUpdate(0);
    setupScrollLoad();
    setupKeyboard();
    if (settings.doubleClickPause && contentAreaEl) {
      contentAreaEl.addEventListener("dblclick", () => {
        if (!state) return;
        state.autoLoadPaused = !state.autoLoadPaused;
      });
    }
    logger.info("阅读视图渲染完成", {
      bookTitle: initialChapter.bookTitle,
      chapterTitle: initialChapter.chapterTitle
    });
  }
  function appendChapter(chapter) {
    if (!state || !contentAreaEl) return;
    state.chapters.push(chapter);
    const index = state.chapters.length - 1;
    const chapterEl = renderChapterElement(chapter, index);
    contentAreaEl.appendChild(chapterEl);
    if (intersectionObserver) {
      intersectionObserver.observe(chapterEl);
    }
    addSidebarItem(chapter, (i) => {
      scrollToChapter(i);
    });
    updateBottomNav(
      {
        prevUrl: chapter.prevUrl,
        indexUrl: chapter.indexUrl,
        nextUrl: chapter.nextUrl
      },
      async (url) => {
        await navigateToChapter(url);
      }
    );
  }
  function scrollToChapter(index) {
    if (!contentAreaEl) return;
    const st = loadAllSettings();
    const target = contentAreaEl.querySelector(`[data-chapter-index="${index}"]`);
    if (target && typeof target.scrollIntoView === "function") {
      target.scrollIntoView({ behavior: st.scrollAnimate ? "smooth" : "auto", block: "start" });
    }
    if (state) {
      state.activeIndex = index;
    }
    setActiveSidebarItem(index);
    applyTitleUpdate(index);
    updateHistory(index);
  }
  async function navigateToChapter(url) {
    if (!rule || isLoadingNext) return;
    isLoadingNext = true;
    removeErrorIndicator();
    showLoadingIndicator();
    const st = loadAllSettings();
    const result = await loadNextChapter(url, rule, textRules, cleanOptions, st.maxRetries, st.retryDelay);
    hideLoadingIndicator();
    if (result.status === "loaded" && result.chapter) {
      appendChapter(result.chapter);
      scrollToChapter(state.chapters.length - 1);
    } else if (result.status === "failed") {
      showErrorIndicator(url);
      logger.warn(`无法加载章节: ${url}`);
    }
    isLoadingNext = false;
  }
  function toggleQuietMode() {
    if (!containerEl) return;
    if (containerEl.classList.contains("nr-quiet")) {
      containerEl.classList.remove("nr-quiet");
      if (state) state.quietMode = false;
    } else {
      containerEl.classList.add("nr-quiet");
      if (state) state.quietMode = true;
    }
  }
  function waitForSelector(selector, doc, timeout = 1e4) {
    return new Promise((resolve) => {
      if (doc.querySelector(selector)) {
        resolve();
        return;
      }
      const observer = new MutationObserver(() => {
        if (doc.querySelector(selector)) {
          observer.disconnect();
          resolve();
        }
      });
      const root = doc.body || doc.documentElement;
      if (root) {
        observer.observe(root, { childList: true, subtree: true });
      }
      setTimeout(() => {
        observer.disconnect();
        resolve();
      }, timeout);
    });
  }
  async function initApp(options) {
    const doc = document;
    const url = location.href;
    const settings = loadAllSettings();
    logger.setDebug(settings.debug);
    logger.info("ReaderApp 启动");
    clearLoadedUrls();
    await initRuleRegistry(settings.siteRulesUrl);
    await initTextRuleRegistry(settings.textRulesUrl);
    const rule2 = matchRule(url);
    if (!rule2) {
      logger.info("当前页面未匹配任何站点规则,跳过");
      return;
    }
    if (rule2.disableAuto) {
      logger.info("站点规则设置 disableAuto,跳过自动启动");
      return;
    }
    if (rule2.waitDelay && rule2.waitDelay > 0) {
      logger.info(`等待 ${rule2.waitDelay}ms 后启动...`);
      await new Promise((resolve) => setTimeout(resolve, rule2.waitDelay));
    }
    if (rule2.waitSelector) {
      logger.info(`等待选择器 "${rule2.waitSelector}" 出现...`);
      await waitForSelector(rule2.waitSelector, doc);
    }
    const textRules2 = getCombinedTextRules();
    const s2tMapping = await loadS2TMapping();
    const cleanOptions2 = {
      convertToTraditional: settings.convertToTraditional,
      splitContent: settings.splitContent,
      s2tMapping
    };
    const chapter = parseChapter(doc, url, rule2, textRules2, cleanOptions2);
    logger.info("章节解析完成", {
      bookTitle: chapter.bookTitle,
      chapterTitle: chapter.chapterTitle,
      contentLength: chapter.contentText.length,
      isVip: chapter.isVip,
      hasNext: !!chapter.nextUrl,
      hasPrev: !!chapter.prevUrl
    });
    renderReaderView(chapter, rule2, textRules2, cleanOptions2);
  }
  function isBooklinkHost(hostname = location.hostname) {
    return hostname === "booklink.me" || hostname.endsWith(".booklink.me");
  }
  function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  function findUnreadParent() {
    const tds = document.querySelectorAll('td[colspan="2"]');
    for (const td of tds) {
      const text = td.textContent || "";
      if (text.includes("未读") || text.includes("未讀")) {
        return td;
      }
    }
    return null;
  }
  function findUnreadLinks(context) {
    const result = [];
    const xpath = './ancestor::table[@width="100%"]/descendant::a[img[@alt="未读"]]';
    const snapshot = document.evaluate(
      xpath,
      context,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    );
    for (let i = 0; i < snapshot.snapshotLength; i++) {
      const node = snapshot.snapshotItem(i);
      if (node instanceof HTMLAnchorElement) {
        result.push(node);
      }
    }
    return result;
  }
  function hasNoPirateMarker(link) {
    const tr = link.closest("tr");
    if (!tr) return false;
    const chapterLink = tr.querySelector("td:last-child a");
    return !!(chapterLink == null ? void 0 : chapterLink.querySelector('font[color*="800000"]'));
  }
  function markClicked(link) {
    const tr = link.closest("tr");
    if (!tr) return;
    const fontEl = tr.querySelector("td:first-child font");
    if (fontEl) {
      fontEl.setAttribute("color", "666666");
    }
    const chapterLink = tr.querySelector("td:last-child a");
    if (chapterLink) {
      chapterLink.classList.add("mclicked");
    }
  }
  function createButton(parent) {
    const btn = document.createElement("a");
    btn.href = "javascript:;";
    btn.title = "一键打开所有未读链接";
    btn.style.cssText = "width:auto;";
    btn.addEventListener("click", async (e) => {
      e.preventDefault();
      const links = findUnreadLinks(btn);
      for (const link of links) {
        if (hasNoPirateMarker(link)) continue;
        await delay(200);
        gmOpenInTab(link.href);
        markClicked(link);
      }
    });
    const img = document.createElement("img");
    img.src = "me.png";
    img.style.cssText = "max-width:20px;";
    btn.appendChild(img);
    parent.appendChild(btn);
  }
  function init() {
    if (!isBooklinkHost()) return;
    const settings = loadAllSettings();
    if (!settings.booklinkEnable) return;
    const parent = findUnreadParent();
    if (!parent) {
      logger.info("booklink.me: 未找到未读区域");
      return;
    }
    createButton(parent);
    logger.info("booklink.me 辅助模式已启动");
  }
  (function() {
    if (window.top !== window.self) {
      return;
    }
    const start = () => {
      logger.info("小说阅读脚本初始化中...");
      if (isBooklinkHost()) {
        init();
        return;
      }
      const settings = loadAllSettings();
      if (settings.disableAutoLaunch) {
        logger.info("自动启动已禁用,渲染手动启动按钮");
        const btn = document.createElement("button");
        btn.className = "nr-manual-start";
        btn.textContent = "📖 阅读模式";
        Object.assign(btn.style, {
          position: "fixed",
          top: "12px",
          left: "12px",
          zIndex: "2147483648",
          padding: "8px 16px",
          fontSize: "14px",
          cursor: "pointer",
          background: "#4a90d9",
          color: "#fff",
          border: "none",
          borderRadius: "4px"
        });
        btn.addEventListener("click", () => {
          btn.remove();
          initApp();
        });
        document.body.appendChild(btn);
        if (typeof unsafeWindow !== "undefined") {
          unsafeWindow.startNovelReader = () => {
            btn.remove();
            return initApp();
          };
        }
        return;
      }
      initApp();
    };
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", start, { once: true });
    } else {
      start();
    }
  })();

})();