Awesome LinuxDo Reader Lite Core Library

Core runtime modules for Awesome LinuxDo Reader Lite.

אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/590254/1904332/Awesome%20LinuxDo%20Reader%20Lite%20Core%20Library.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Awesome LinuxDo Reader Lite Core Library
// @name:zh-CN   Awesome LinuxDo Reader Lite 核心库
// @namespace    https://github.com/sunbigfly/awesome-linuxdo-reader
// @version      1.5.1
// @description  Core runtime and presentation modules for Awesome LinuxDo Reader Lite.
// @description:zh-CN 应用、Shell、主题、流、布局与 userscript 运行核心
// @author       sunbigfly
// @license      MIT
// @homepageURL  https://github.com/sunbigfly/awesome-linuxdo-reader
// @supportURL   https://github.com/sunbigfly/awesome-linuxdo-reader/issues
// @match        https://linux.do/*
// @grant        none
// ==/UserScript==

/* Awesome LinuxDo Reader Lite 1.5.1 - main-lite-core
 * 应用、Shell、主题、流、布局与 userscript 运行核心
 * 项目 TypeScript 源码保持可读;固定版本第三方依赖压缩打包。
 * 不要直接编辑此文件;修改 lite/src 后重新构建。
 */
(function () {
	'use strict';

	const root = globalThis;
	const runtimeKey = "__AWESOME_LINUXDO_READER_LITE_MODULE_RUNTIME__";
	let runtime = root[runtimeKey];
	if (runtime === undefined) {
		const factories = new Map();
		const sourceHashes = new Map();
		const modules = new Map();
		const libraries = new Set();
		let started = false;
		const externalModuleIds = Object.freeze({"@xsai/generate-text":"vendor/xsai-generate-text.js"});

		const resolve = (parentId, request) => {
			const externalId = externalModuleIds[request];
			if (externalId) return externalId;
			if (!request.startsWith('.')) {
				throw new Error(`[main-lite] unsupported external module: ${request}`);
			}
			const parts = parentId.split('/');
			parts.pop();
			for (const part of request.split('/')) {
				if (!part || part === '.') continue;
				if (part === '..') {
					if (!parts.length) {
						throw new Error(`[main-lite] module escapes root: ${parentId} -> ${request}`);
					}
					parts.pop();
				} else {
					parts.push(part);
				}
			}
			const resolved = parts.join('/');
			return /\.(?:js|json)$/.test(resolved) ? resolved : `${resolved}.js`;
		};

		const requireModule = (id) => {
			const cached = modules.get(id);
			if (cached) return cached.exports;
			const factory = factories.get(id);
			if (!factory) throw new Error(`[main-lite] missing module: ${id}`);
			const module = { exports: {} };
			modules.set(id, module);
			try {
				factory(module, module.exports, (request) => (
					requireModule(resolve(id, request))
				));
			} catch (error) {
				modules.delete(id);
				throw error;
			}
			return module.exports;
		};

		runtime = Object.freeze({
			schemaVersion: 1,
			sourceVersion: "1.5.1",
			register(id, factory, sourceHash) {
				const currentHash = sourceHashes.get(id);
				if (currentHash !== undefined) {
					if (currentHash !== sourceHash) {
						throw new Error(`[main-lite] conflicting module: ${id}`);
					}
					return;
				}
				factories.set(id, factory);
				sourceHashes.set(id, sourceHash);
			},
			markLibrary(name) {
				libraries.add(name);
			},
			start(entryId, expectedLibraries) {
				for (const name of expectedLibraries) {
					if (!libraries.has(name)) {
						throw new Error(`[main-lite] missing library: ${name}`);
					}
				}
				if (started) return requireModule(entryId);
				started = true;
				try {
					return requireModule(entryId);
				} catch (error) {
					started = false;
					throw error;
				}
			},
		});
		Object.defineProperty(root, runtimeKey, {
			configurable: true,
			enumerable: false,
			writable: false,
			value: runtime,
		});
	}
	if (runtime.schemaVersion !== 1 || runtime.sourceVersion !== "1.5.1") {
		throw new Error('[main-lite] Library 版本不匹配');
	}

	var __defProp = Object.defineProperty;
	var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
	var __getOwnPropNames = Object.getOwnPropertyNames;
	var __hasOwnProp = Object.prototype.hasOwnProperty;
	var __export = (target, all) => {
	  for (var name in all)
	    __defProp(target, name, { get: all[name], enumerable: !0 });
	};
	var __copyProps = (to, from, except, desc) => {
	  if (from && typeof from == "object" || typeof from == "function")
	    for (let key of __getOwnPropNames(from))
	      !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
	  return to;
	};
	var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: !0 }), mod);
	var __create = Object.create;
	var __getProtoOf = Object.getPrototypeOf;
	var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
	  // If the importer is in node compatibility mode or this is not an ESM
	  // file that has been converted to a CommonJS file using a Babel-
	  // compatible transform (i.e. "__esModule" has not been set), then set
	  // "default" to the CommonJS "module.exports" for node compatibility.
	  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target,
	  mod
	));

/* Source: lite/src/app/reader-application-stages.ts */
runtime.register("src/app/reader-application-stages.js", function(module, exports, require) {
	var reader_application_stages_exports = {};
	__export(reader_application_stages_exports, {
	  createPreferencesStorageSyncStage: () => createPreferencesStorageSyncStage
	});
	module.exports = __toCommonJS(reader_application_stages_exports);
	function createPreferencesStorageSyncStage(options) {
	  const key = String(options.key).trim();
	  if (!key) throw new Error("preferences storage key 不能为空");
	  const windowPort = options.window ?? window;
	  return Object.freeze({
	    name: "preferences-storage-sync",
	    required: !1,
	    setup: (scope) => {
	      scope.listen(windowPort, "storage", (rawEvent) => {
	        const event = rawEvent;
	        if (!(event.key !== key && event.key !== null || event.key === key && event.oldValue === event.newValue))
	          try {
	            options.repository.reloadExternal();
	          } catch (cause) {
	            options.onError?.(cause);
	          }
	      });
	    }
	  });
	}
}, "9ba835be1c1dd532abf442a2488ce815108b4324cf870d46ed435f814888efac");

/* Source: lite/src/app/reader-application.ts */
runtime.register("src/app/reader-application.js", function(module, exports, require) {
	var reader_application_exports = {};
	__export(reader_application_exports, {
	  BrowserDiscourseHostPort: () => BrowserDiscourseHostPort,
	  ReaderApplication: () => ReaderApplication,
	  browserBodyReady: () => browserBodyReady,
	  detectDiscourseHost: () => detectDiscourseHost
	});
	module.exports = __toCommonJS(reader_application_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function abortError(reason) {
	  return reason instanceof Error ? reason : new DOMException("Reader application 已销毁", "AbortError");
	}
	function stageName(value) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error("application stage name 不能为空");
	  return normalized;
	}
	class ReaderApplication {
	  changes = new import_signal.Signal();
	  diagnostics = new import_signal.Signal();
	  #options;
	  #scope = new import_lifecycle.LifecycleScope();
	  #controller = new AbortController();
	  #state = "idle";
	  #startPromise = null;
	  constructor(options) {
	    const names = options.stages.map((stage) => stageName(stage.name));
	    if (new Set(names).size !== names.length)
	      throw new Error("application stage name 不能重复");
	    this.#options = options, this.#scope.add(() => {
	      this.#controller.signal.aborted || this.#controller.abort(abortError(null));
	    });
	  }
	  get state() {
	    return this.#state;
	  }
	  get scope() {
	    return this.#scope;
	  }
	  start() {
	    return this.#startPromise ? this.#startPromise : this.#state === "destroyed" ? Promise.resolve("destroyed") : (this.#startPromise = this.#run(), this.#startPromise);
	  }
	  destroy() {
	    this.#state !== "destroyed" && (this.#setState("destroyed"), this.#scope.destroy());
	  }
	  async #run() {
	    try {
	      this.#setState("waiting-body"), await this.#options.bodyReady(this.#controller.signal), this.#throwIfDestroyed();
	      let preferences = this.#options.preferences.load().value;
	      const preferenceChanges = new import_signal.Signal();
	      this.#scope.add(() => preferenceChanges.clear());
	      const publishPreferences = (value) => {
	        if (preferences !== value) {
	          preferences = value;
	          for (const cause of preferenceChanges.emit(preferences))
	            this.diagnostics.emit(Object.freeze({
	              stage: "preferences-live",
	              required: !1,
	              cause
	            }));
	        }
	      }, stopPreferenceChanges = this.#options.preferences.changes?.subscribe((snapshot) => {
	        publishPreferences(snapshot.value);
	      }, this.#scope);
	      this.#setState("waiting-host");
	      const host = await this.#options.host.waitForHost(this.#controller.signal);
	      if (this.#throwIfDestroyed(), !host)
	        return stopPreferenceChanges?.(), this.#setState("skipped"), this.#state;
	      this.#setState("starting");
	      const context = Object.freeze({
	        preferences,
	        readPreferences: () => preferences,
	        ...this.#options.preferences.update === void 0 ? {} : {
	          updatePreferences: (patch) => {
	            this.#throwIfDestroyed();
	            const snapshot = this.#options.preferences.update(patch);
	            return publishPreferences(snapshot.value), snapshot.value;
	          }
	        },
	        preferenceChanges,
	        host
	      });
	      for (const stage of this.#options.stages) {
	        this.#throwIfDestroyed();
	        const child = this.#scope.child();
	        try {
	          const cleanup = await stage.setup(child, context);
	          typeof cleanup == "function" && child.add(cleanup), this.#throwIfDestroyed();
	        } catch (cause) {
	          try {
	            child.destroy();
	          } catch (cleanupCause) {
	            this.diagnostics.emit(Object.freeze({
	              stage: `${stage.name}:cleanup`,
	              required: stage.required,
	              cause: cleanupCause
	            }));
	          }
	          if (this.diagnostics.emit(Object.freeze({
	            stage: stage.name,
	            required: stage.required,
	            cause
	          })), stage.required) throw cause;
	        }
	      }
	      return this.#throwIfDestroyed(), this.#setState("running"), this.#state;
	    } catch {
	      if (this.#state === "destroyed" || this.#controller.signal.aborted)
	        return "destroyed";
	      this.#setState("failed");
	      try {
	        this.#scope.destroy();
	      } catch (cleanupCause) {
	        this.diagnostics.emit(Object.freeze({
	          stage: "application:cleanup",
	          required: !0,
	          cause: cleanupCause
	        }));
	      }
	      return this.#state;
	    }
	  }
	  #throwIfDestroyed() {
	    if (this.#state === "destroyed" || this.#controller.signal.aborted)
	      throw abortError(this.#controller.signal.reason);
	  }
	  #setState(state) {
	    this.#state !== state && (this.#state = state, this.changes.emit(state));
	  }
	}
	function detectDiscourseHost(moduleLookup, documentPort) {
	  try {
	    if (moduleLookup("discourse/lib/url"))
	      return Object.freeze({ detection: "native-module" });
	  } catch {
	  }
	  return documentPort.querySelector(
	    'meta[name="generator"][content*="Discourse" i],meta[name="discourse_theme_id"],#data-preloaded,#ember-app .d-header,#ember-app .topic-list,#ember-app .topic-post'
	  ) ? Object.freeze({ detection: "dom-marker" }) : null;
	}
	class BrowserDiscourseHostPort {
	  #moduleLookup;
	  #document;
	  #window;
	  #timeoutMs;
	  #createObserver;
	  constructor(options) {
	    this.#moduleLookup = options.moduleLookup, this.#document = options.document ?? document, this.#window = options.window ?? window;
	    const timeoutMs = options.timeoutMs ?? 15e3;
	    if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 12e4)
	      throw new RangeError("Discourse host timeout 必须是 1..120000 的安全整数");
	    this.#timeoutMs = timeoutMs, this.#createObserver = options.createObserver ?? ((callback) => new MutationObserver(callback));
	  }
	  waitForHost(signal) {
	    const immediate = detectDiscourseHost(this.#moduleLookup, this.#document);
	    return immediate ? Promise.resolve(immediate) : signal.aborted ? Promise.reject(abortError(signal.reason)) : this.#document.readyState === "complete" ? Promise.resolve(null) : new Promise((resolve, reject) => {
	      let settled = !1, timer = 0;
	      const observer = this.#createObserver?.(() => check()), cleanup = () => {
	        timer && this.#window.clearTimeout(timer), observer?.disconnect(), this.#window.removeEventListener("load", onLoad), signal.removeEventListener("abort", onAbort);
	      }, finish = (value) => {
	        settled || (settled = !0, cleanup(), resolve(value));
	      }, check = () => {
	        const detected = detectDiscourseHost(this.#moduleLookup, this.#document);
	        return detected && finish(detected), detected;
	      }, onLoad = () => {
	        finish(check());
	      }, onAbort = () => {
	        settled || (settled = !0, cleanup(), reject(abortError(signal.reason)));
	      };
	      observer?.observe(this.#document.documentElement, {
	        childList: !0,
	        subtree: !0
	      }), this.#window.addEventListener("load", onLoad), signal.addEventListener("abort", onAbort, { once: !0 }), timer = this.#window.setTimeout(() => {
	        finish(detectDiscourseHost(this.#moduleLookup, this.#document));
	      }, this.#timeoutMs);
	    });
	  }
	}
	function browserBodyReady(documentPort, signal) {
	  return documentPort.body ? Promise.resolve() : signal.aborted ? Promise.reject(abortError(signal.reason)) : new Promise((resolve, reject) => {
	    const cleanup = () => {
	      documentPort.removeEventListener("DOMContentLoaded", onReady), signal.removeEventListener("abort", onAbort);
	    }, onReady = () => {
	      cleanup(), resolve();
	    }, onAbort = () => {
	      cleanup(), reject(abortError(signal.reason));
	    };
	    documentPort.addEventListener("DOMContentLoaded", onReady, { once: !0 }), signal.addEventListener("abort", onAbort, { once: !0 });
	  });
	}
}, "06e20cfe8a3ff2bb72a0055cda065487e96fe86b16de928d7c7f9b70a9f770ce");

/* Source: lite/src/app/reader-browser-runtime.ts */
runtime.register("src/app/reader-browser-runtime.js", function(module, exports, require) {
	var reader_browser_runtime_exports = {};
	__export(reader_browser_runtime_exports, {
	  ReaderBrowserRuntime: () => ReaderBrowserRuntime,
	  createReaderBrowserRuntimeStage: () => createReaderBrowserRuntimeStage,
	  readerWebDavCacheClearPlan: () => readerWebDavCacheClearPlan
	});
	module.exports = __toCommonJS(reader_browser_runtime_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_native_request_descriptors = require("../discourse/native-request-descriptors.js"), import_reader_cache_management_surface = require("../cache/reader-cache-management-surface.js"), import_reader_collection_page_repository = require("../cache/reader-collection-page-repository.js"), import_browser_asset_cache = require("../cache/browser-asset-cache.js"), import_discourse_application_cache_invalidation = require("../cache/discourse-application-cache-invalidation.js"), import_reader_settings_config_manager = require("../state/reader-settings-config-manager.js"), import_native_composer = require("../discourse/native-composer.js"), import_reader_control_tooltip = require("../components/reader-control-tooltip.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_native_composer_window = require("../discourse/reader-native-composer-window.js"), import_reader_native_post_admin_menu = require("../discourse/reader-native-post-admin-menu.js"), import_native_post_model_factory = require("../discourse/native-post-model-factory.js"), import_native_presence = require("../discourse/native-presence.js"), import_identifiers = require("../discourse/identifiers.js"), import_reader_history_navigation_controller = require("../history/reader-history-navigation-controller.js"), import_reader_history_model = require("../history/reader-history-model.js"), import_reader_history_repository = require("../history/reader-history-repository.js"), import_reader_history_navigation_view = require("../history/reader-history-navigation-view.js"), import_reader_history_panel_view = require("../history/reader-history-panel-view.js"), import_reader_chronicle_repository = require("../history/reader-chronicle-repository.js"), import_reader_chronicle_view = require("../history/reader-chronicle-view.js"), import_reader_unwanted_topic_repository = require("../collection/reader-unwanted-topic-repository.js"), import_reader_unwanted_topic_view = require("../collection/reader-unwanted-topic-view.js"), import_discourse_bookmark_adapter = require("../bookmark/discourse-bookmark-adapter.js"), import_reader_bookmark_controller = require("../bookmark/reader-bookmark-controller.js"), import_reader_bookmark_panel_view = require("../bookmark/reader-bookmark-panel-view.js"), import_reader_bookmark_model = require("../bookmark/reader-bookmark-model.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_post_author_filter_feature = require("../topic/reader-post-author-filter-feature.js"), import_signal = require("../kernel/signal.js"), import_reader_topic_live_navigation_controller = require("../live/reader-topic-live-navigation-controller.js"), import_reader_resource_monitor = require("../monitor/reader-resource-monitor.js"), import_browser_request_observation = require("../network/browser-request-observation.js"), import_reader_topic_live_navigation_view = require("../live/reader-topic-live-navigation-view.js"), import_reader_rate_limit_notice = require("../shell/reader-rate-limit-notice.js"), import_reader_escape_surface = require("../shell/reader-escape-surface.js"), import_reader_image_download_service = require("../media/reader-image-download-service.js"), import_reader_image_resource_service = require("../media/reader-image-resource-service.js"), import_reader_lightbox_image_picker = require("../media/reader-lightbox-image-picker.js"), import_reader_media_prefetch_service = require("../media/reader-media-prefetch-service.js"), import_reader_topic_image_index = require("../media/reader-topic-image-index.js"), import_reader_topic_image_interaction = require("../media/reader-topic-image-interaction.js"), import_reader_lightbox_feature = require("../media/reader-lightbox-feature.js"), import_reader_compact_image_viewer = require("../media/reader-compact-image-viewer.js"), import_reader_topic_media_feature = require("../media/reader-topic-media-feature.js"), import_reader_media_controller = require("../media/reader-media-controller.js"), import_reader_cooked_content_feature = require("../media/reader-cooked-content-feature.js"), import_reader_poll_feature = require("../media/reader-poll-feature.js"), import_browser_shared_request_permit = require("../network/browser-shared-request-permit.js"), import_coordinated_request_client = require("../network/coordinated-request-client.js"), import_discourse_native_read_transport = require("../network/discourse-native-read-transport.js"), import_public_resource_request_adapter = require("../network/public-resource-request-adapter.js"), import_discourse_native_user_port = require("../user/discourse-native-user-port.js"), import_reader_user_domain_session = require("../user/reader-user-domain-session.js"), import_reader_credit_account_adapter = require("../user/reader-credit-account-adapter.js"), import_reader_connect_trust_adapter = require("../user/reader-connect-trust-adapter.js"), import_reader_user_endorsement_adapter = require("../user/reader-user-endorsement-adapter.js"), import_reader_user_card_view = require("../user/reader-user-card-view.js"), import_discourse_user_observation_adapter = require("../user/discourse-user-observation-adapter.js"), import_reader_user_observation_session = require("../user/reader-user-observation-session.js"), import_reader_user_observation_page_repository = require("../user/reader-user-observation-page-repository.js"), import_reader_user_observation_model = require("../user/reader-user-observation-model.js"), import_reader_user_observation_view = require("../user/reader-user-observation-view.js"), import_reader_self_observation_projection = require("../user/reader-self-observation-projection.js"), import_reader_settings_user_view = require("../user/reader-settings-user-view.js"), import_discourse_notification_adapter = require("../notification/discourse-notification-adapter.js"), import_reader_notification_controller = require("../notification/reader-notification-controller.js"), import_reader_notification_model = require("../notification/reader-notification-model.js"), import_reader_notification_panel_view = require("../notification/reader-notification-panel-view.js"), import_action_request_adapter = require("../post/action-request-adapter.js"), import_discourse_action_transport = require("../post/discourse-action-transport.js"), import_post_action_controller = require("../post/post-action-controller.js"), import_discourse_action_descriptors = require("../post/discourse-action-descriptors.js"), import_post_action_feature_commands = require("../post/post-action-feature-commands.js"), import_user_action_feature_commands = require("../post/user-action-feature-commands.js"), import_reader_post_action_feature = require("../post/reader-post-action-feature.js"), import_reader_bookmark_action_coordinator = require("../post/reader-bookmark-action-coordinator.js"), import_reader_share_action_coordinator = require("../post/reader-share-action-coordinator.js"), import_reader_topic_notification_coordinator = require("../post/reader-topic-notification-coordinator.js"), import_reader_topic_shared_issue_coordinator = require("../post/reader-topic-shared-issue-coordinator.js"), import_topic_action_feature_commands = require("../post/topic-action-feature-commands.js"), import_reader_topic_action_rail = require("../post/reader-topic-action-rail.js"), import_reader_topic_summary_request_adapter = require("../post/reader-topic-summary-request-adapter.js"), import_reader_topic_summary_surface = require("../post/reader-topic-summary-surface.js"), import_reader_topic_custom_summary = require("../post/reader-topic-custom-summary.js"), import_reader_post_management_action_coordinator = require("../post/reader-post-management-action-coordinator.js"), import_reader_selection_quote_feature = require("../post/reader-selection-quote-feature.js"), import_reader_workspace_coordinator = require("../shell/reader-workspace-coordinator.js"), import_reader_feedback_surface = require("../shell/reader-feedback-surface.js"), import_reader_action_surface_coordinator = require("../shell/reader-action-surface-coordinator.js"), import_reader_exclusive_panel_coordinator = require("../shell/reader-exclusive-panel-coordinator.js"), import_reader_shell_recovery_view = require("../shell/reader-shell-recovery-view.js"), import_reader_report_form_surface = require("../shell/reader-report-form-surface.js"), import_reader_assignment_form_surface = require("../shell/reader-assignment-form-surface.js"), import_reader_choice_form_surface = require("../shell/reader-choice-form-surface.js"), import_reader_topic_edit_form_surface = require("../shell/reader-topic-edit-form-surface.js"), import_reader_settings_controller = require("../settings/reader-settings-controller.js"), import_reader_settings_view = require("../settings/reader-settings-view.js"), import_reader_theme_settings_control = require("../settings/reader-theme-settings-control.js"), import_reader_window_settings_form = require("../settings/reader-window-settings-form.js"), import_reader_shortcut_settings_form = require("../settings/reader-shortcut-settings-form.js"), import_reader_custom_site_settings_form = require("../settings/reader-custom-site-settings-form.js"), import_reader_webdav_settings_form = require("../settings/reader-webdav-settings-form.js"), import_browser_discourse_site_probe = require("../site/browser-discourse-site-probe.js"), import_reader_webdav_coordinator = require("../sync/reader-webdav-coordinator.js"), import_reader_webdav_category_ports = require("../sync/reader-webdav-category-ports.js"), import_reader_performance_settings_form = require("../settings/reader-performance-settings-form.js"), import_reader_reading_settings_form = require("../settings/reader-reading-settings-form.js"), import_reader_translation_settings_form = require("../settings/reader-translation-settings-form.js"), import_reader_ai_service_settings_form = require("../settings/reader-ai-service-settings-form.js"), import_reader_appearance_settings_form = require("../settings/reader-appearance-settings-form.js"), import_reader_font_settings_form = require("../settings/reader-font-settings-form.js"), import_reader_motion_settings_form = require("../settings/reader-motion-settings-form.js"), import_reader_layout_settings_form = require("../settings/reader-layout-settings-form.js"), import_reader_interaction_settings_form = require("../settings/reader-interaction-settings-form.js"), import_reader_image_settings_form = require("../settings/reader-image-settings-form.js"), import_reader_select_surface = require("../shell/reader-select-surface.js"), import_reader_floating_window_frame = require("../shell/reader-floating-window-frame.js"), import_reader_image_preferences = require("../media/reader-image-preferences.js"), import_reader_reply_tree_preferences = require("../topic/reader-reply-tree-preferences.js"), import_reader_about_settings_content = require("../settings/reader-about-settings-content.js"), import_reader_open_queue_session = require("../queue/reader-open-queue-session.js"), import_reader_topic_download_manager = require("../queue/reader-topic-download-manager.js"), import_reader_topic_offline_document = require("../archive/reader-topic-offline-document.js"), import_reader_topic_offline_artifact_repository = require("../archive/reader-topic-offline-artifact-repository.js"), import_reader_cooked_content_feature2 = require("../media/reader-cooked-content-feature.js"), import_reader_katex_controller = require("../media/reader-katex-controller.js"), import_reader_shortcut_controller = require("../shell/reader-shortcut-controller.js"), import_reader_appearance_style_controller = require("../appearance/reader-appearance-style-controller.js"), import_reader_theme_controller = require("../appearance/reader-theme-controller.js"), import_reader_font_style_controller = require("../font/reader-font-style-controller.js"), import_reader_loading_animation_view = require("../motion/reader-loading-animation-view.js"), import_reader_layout_style_controller = require("../layout/reader-layout-style-controller.js"), import_reader_topic_factory = require("../topic/reader-topic-factory.js"), import_reader_topic_navigation_controller = require("../topic/reader-topic-navigation-controller.js"), import_reader_native_topic_route = require("../topic/reader-native-topic-route.js"), import_reader_topic_flow_controller = require("../topic/reader-topic-flow-controller.js"), import_reader_topic_navigation_preferences = require("../topic/reader-topic-navigation-preferences.js"), import_reader_topic_scroll_adapter = require("../topic/reader-topic-scroll-adapter.js"), import_reader_topic_local_archive_feature = require("../topic/reader-topic-local-archive-feature.js"), import_reader_topic_timeline_controller = require("../topic/reader-topic-timeline-controller.js"), import_reader_topic_timeline_view = require("../topic/reader-topic-timeline-view.js"), import_reader_topic_header = require("../topic/reader-topic-header.js"), import_reader_topic_edit_controller = require("../topic/reader-topic-edit-controller.js"), import_reader_topic_comments_header = require("../topic/reader-topic-comments-header.js"), import_reader_topic_only_op_controller = require("../topic/reader-topic-only-op-controller.js"), import_reader_topic_special_content_feature = require("../topic/reader-topic-special-content-feature.js"), import_reader_topic_context_controller = require("../topic/reader-topic-context-controller.js"), import_reader_topic_context_surface = require("../topic/reader-topic-context-surface.js"), import_reader_topic_context_state = require("../topic/reader-topic-context-state.js"), import_topic_session = require("../topic/topic-session.js"), import_reader_translation_feature = require("../translation/reader-translation-feature.js"), import_translation_request_adapter = require("../translation/translation-request-adapter.js"), import_reader_data_runtime = require("./reader-data-runtime.js"), import_reader_performance_policy = require("./reader-performance-policy.js");
	const readerSurfaceOnlyCloseEvents = /* @__PURE__ */ new WeakSet(), hostTopicUserCardSelector = "html.ldp-reader-workspace :is(.topic-list-item,.latest-topic-list-item) :is(.posters,.topic-poster) [data-user-card]";
	function readerWebDavCacheClearPlan(categories) {
	  const webDavCategories = [], protectedCategories = [];
	  return categories.includes("history") && (webDavCategories.push("history"), protectedCategories.push("history")), categories.includes("notifications") && (webDavCategories.push("notification-history"), protectedCategories.push("notifications")), categories.includes("responses") && (webDavCategories.push(
	    "bookmarks",
	    "translation-cache",
	    "activity-history"
	  ), protectedCategories.push("responses")), Object.freeze({
	    webDavCategories: Object.freeze(webDavCategories),
	    protectedCategories: Object.freeze(protectedCategories)
	  });
	}
	function readerReportOptions(document, flagTypes, availableNames, appliesTo) {
	  return Object.freeze(flagTypes.filter(
	    (flag) => flag.enabled && availableNames.has(flag.nameKey) && (!flag.appliesTo.length || flag.appliesTo.includes(appliesTo))
	  ).map((flag) => {
	    const template = document.createElement("template");
	    template.innerHTML = flag.description;
	    const description = String(
	      template.content.textContent ?? ""
	    ).replace(/\s+/g, " ").trim();
	    return Object.freeze({
	      id: flag.id,
	      label: flag.label,
	      description: description || "提交给社区管理人员审核。",
	      requireMessage: flag.requireMessage
	    });
	  }));
	}
	function readerNativeModelValue(value, key) {
	  if (!value || typeof value != "object" && typeof value != "function")
	    return;
	  const model = value;
	  return typeof model.get == "function" ? model.get(key) : model[key];
	}
	function readerPollViewer(currentUser, fallbackUsername) {
	  const id = Number(readerNativeModelValue(currentUser, "id")), username = String(
	    readerNativeModelValue(currentUser, "username") ?? fallbackUsername
	  ).trim(), rawGroups = readerNativeModelValue(currentUser, "groups"), groups = Array.isArray(rawGroups) ? rawGroups.map((group) => String(
	    readerNativeModelValue(group, "name") ?? group
	  ).trim()).filter(Boolean) : [];
	  return Object.freeze({
	    id: Number.isSafeInteger(id) && id > 0 ? id : null,
	    username: username || null,
	    staff: ["staff", "admin", "moderator"].some((key) => readerNativeModelValue(currentUser, key) === !0),
	    groups: Object.freeze(groups)
	  });
	}
	function readerShellElement(root, selector, owner) {
	  const value = root.querySelector(selector);
	  if (!value) throw new Error(`${owner} 缺少 Shell 命名控件:${selector}`);
	  return value;
	}
	function readerTopicHeaderElements(root) {
	  const query = (selector) => readerShellElement(root, selector, "主题 Header");
	  return Object.freeze({
	    titleJump: query(".ldp-title-jump"),
	    metaHost: query(".ldp-meta"),
	    metaStats: query(".ldp-meta-stats"),
	    metaOwner: query(".ldp-meta-owner"),
	    metaOwnerValue: query(".ldp-meta-owner-value"),
	    onlyOpToggle: query(".ldp-only-op-toggle"),
	    onlyOpProgress: query(".ldp-only-op-progress"),
	    onlyOpProgressValue: query(".ldp-only-op-progress-value"),
	    topicIdentityHost: query(".ldp-title-topic-row")
	  });
	}
	function readerTopicTimelineElements(root) {
	  const query = (selector) => readerShellElement(root, selector, "时间轴 View");
	  return Object.freeze({
	    root,
	    timeline: query(".ldp-topic-timeline"),
	    date: query(".ldp-topic-timeline-date"),
	    track: query(".ldp-topic-timeline-track"),
	    cursor: query(".ldp-topic-timeline-cursor"),
	    current: query(".ldp-topic-timeline-current"),
	    total: query(".ldp-topic-timeline-total"),
	    preview: query(".ldp-topic-timeline-preview"),
	    relative: query(".ldp-topic-timeline-relative"),
	    jump: query(".ldp-topic-timeline-jump"),
	    top: query(".ldp-topic-timeline-top"),
	    jumpForm: query(".ldp-topic-timeline-jump-form"),
	    jumpInput: query(".ldp-topic-timeline-jump-input"),
	    jumpSubmit: query(
	      ".ldp-topic-timeline-jump-submit"
	    ),
	    jumpHint: query(".ldp-topic-timeline-jump-hint")
	  });
	}
	function readerTopicLiveNavigationElements(root) {
	  const liveRoot = root.querySelector(".ldp-live-update"), jump = root.querySelector(".ldp-live-update-jump"), label = jump?.querySelector("span") ?? null, dismiss = root.querySelector(
	    ".ldp-live-update-dismiss"
	  );
	  if (!liveRoot || !jump || !label || !dismiss)
	    throw new Error("实时新回复 View 缺少 Shell 命名控件");
	  return Object.freeze({
	    root: liveRoot,
	    jump,
	    label,
	    dismiss
	  });
	}
	function readerNotificationPanelElements(root) {
	  const query = (selector) => readerShellElement(root, selector, "消息面板"), modeTabs = [...root.querySelectorAll(
	    ".ldp-notification-mode-tab"
	  )], groupPanels = [...root.querySelectorAll(
	    "[data-notification-mode-panel]"
	  )], groupTabs = [...root.querySelectorAll(
	    ".ldp-notification-tab"
	  )];
	  if (modeTabs.length !== 2 || groupPanels.length !== 2 || groupTabs.length !== 13)
	    throw new Error("消息面板必须提供 2 个模式与完整 13 个分类锚点");
	  return Object.freeze({
	    root,
	    toggle: query(".ldp-notifications-toggle"),
	    badge: query(".ldp-notification-unread-badge"),
	    popover: query(".ldp-notifications-popover"),
	    modeTabs: Object.freeze(modeTabs),
	    groupPanels: Object.freeze(groupPanels),
	    groupTabs: Object.freeze(groupTabs),
	    toolbar: query(".ldp-notification-toolbar"),
	    unreadStatus: query(".ldp-notification-unread-status"),
	    markAll: query(".ldp-notification-mark-all"),
	    newMessage: query(".ldp-notification-new-message"),
	    search: query(".ldp-notification-search"),
	    searchClear: query(".ldp-notification-search-clear"),
	    categoryFilter: query(
	      ".ldp-notification-category-filter"
	    ),
	    tagFilter: query(".ldp-notification-tag-filter"),
	    list: query(".ldp-notification-list"),
	    pagePrevious: query(".ldp-notification-page-prev"),
	    pageInfo: query(".ldp-notification-page-info"),
	    pageNext: query(".ldp-notification-page-next")
	  });
	}
	function readerBookmarkPanelElements(root) {
	  const query = (selector) => readerShellElement(root, selector, "收藏面板"), tabs = [...root.querySelectorAll(
	    ".ldp-bookmark-tab"
	  )];
	  if (tabs.length !== 5)
	    throw new Error(
	      "收藏面板必须提供回应、Boost、回复、帖子、楼层五个分类锚点"
	    );
	  return Object.freeze({
	    root,
	    toggle: query(".ldp-bookmarks-toggle"),
	    popover: query(".ldp-bookmarks-popover"),
	    tabs: Object.freeze(tabs),
	    defaultActions: query(".ldp-bookmarks-default-actions"),
	    multiButton: query(".ldp-bookmarks-multi"),
	    bulkActions: query(".ldp-bookmarks-bulk-actions"),
	    selectScope: query(".ldp-bookmarks-select-scope"),
	    selectToggle: query(".ldp-bookmarks-select-toggle"),
	    deleteSelected: query(
	      ".ldp-bookmarks-delete-selected"
	    ),
	    deleteSelectedLabel: query(
	      ".ldp-bookmarks-delete-selected-label"
	    ),
	    multiDone: query(".ldp-bookmarks-multi-done"),
	    search: query(".ldp-bookmarks-search"),
	    searchClear: query(".ldp-bookmarks-search-clear"),
	    categoryFilter: query(
	      ".ldp-bookmarks-category-filter"
	    ),
	    tagFilter: query(".ldp-bookmarks-tag-filter"),
	    reactionFilters: query(".ldp-reaction-filters"),
	    list: query(".ldp-bookmarks-list"),
	    pagePrevious: query(".ldp-bookmarks-page-prev"),
	    pageInfo: query(".ldp-bookmarks-page-info"),
	    pageNext: query(".ldp-bookmarks-page-next")
	  });
	}
	function readerShellFailureKind(cause) {
	  if (cause && typeof cause == "object") {
	    const kind = "kind" in cause ? String(cause.kind ?? "") : "";
	    if ([
	      "cloudflare",
	      "rate-limit",
	      "authentication",
	      "forbidden",
	      "not-found",
	      "conflict",
	      "validation",
	      "client",
	      "timeout",
	      "server"
	    ].includes(kind))
	      return kind;
	    if ("cloudflareMitigated" in cause && cause.cloudflareMitigated === !0)
	      return "cloudflare";
	    const status = "status" in cause ? Number(cause.status) : 0;
	    if (status === 400 || status === 422) return "validation";
	    if (status === 401) return "authentication";
	    if (status === 403) return "forbidden";
	    if (status === 404 || status === 410) return "not-found";
	    if (status === 429) return "rate-limit";
	    if (status === 408) return "timeout";
	    if (status === 409 || status === 412) return "conflict";
	    if (status === 425 || status >= 500 && status <= 599) return "server";
	    if (status >= 400 && status <= 499) return "client";
	    const name = "name" in cause ? String(cause.name ?? "") : "";
	    if (name === "TimeoutError") return "timeout";
	    if (name === "TypeError" || name === "NetworkError" || name === "OfflineError")
	      return "network";
	  }
	  return "unknown";
	}
	function readerShellRecoveryFailure(cause, challengeHref) {
	  const kind = readerShellFailureKind(cause), copy = {
	    cloudflare: [
	      "Cloudflare 验证尚未完成",
	      "新的 Reader 请求已暂停,后台不会继续打开新验证页;请手动完成唯一验证后重新加载。"
	    ],
	    "rate-limit": [
	      "请求收到 429",
	      "当前逻辑请求已按 Retry-After 有界等待;固定预防窗口仍保留真实启动记录。稍后可手动重试,无需刷新页面或清缓存。"
	    ],
	    authentication: [
	      "登录状态已失效",
	      "服务器返回 401;请先在原站恢复登录,再重新加载当前帖子。"
	    ],
	    forbidden: [
	      "当前请求无权限",
	      "服务器返回 403;该请求不会被当成 429,也不会触发限流恢复。"
	    ],
	    "not-found": [
	      "帖子或楼层不存在",
	      "服务器返回 404/410;内容可能已删除、移动或当前账号不可见。"
	    ],
	    conflict: [
	      "请求状态发生冲突",
	      "服务器返回 409/412;保留当前页面状态,刷新对应内容后再试。"
	    ],
	    validation: [
	      "请求内容未通过校验",
	      "服务器返回 400/422;不会自动重放,请检查当前输入或页面状态。"
	    ],
	    client: [
	      "请求被服务器拒绝",
	      "这是当前请求自身的 4xx 异常;不会升级成 429 或全局验证闸门。"
	    ],
	    timeout: [
	      "请求超时",
	      "有界自动重试仍未恢复;可检查网络后手动重试。"
	    ],
	    network: [
	      "网络暂时不可用",
	      "当前 Topic 状态未被伪造或清空;网络恢复后可手动重试。"
	    ],
	    server: [
	      "服务器暂时不可用",
	      "有界自动重试仍未取得成功响应;稍后可继续手动重试。"
	    ],
	    unknown: [
	      "帖子加载失败",
	      "当前失败已保留为诊断事实;可手动重试或关闭阅读器。"
	    ]
	  };
	  return Object.freeze({
	    kind,
	    message: copy[kind][0],
	    detail: copy[kind][1],
	    ...kind === "cloudflare" && challengeHref ? { challengeHref } : {}
	  });
	}
	function readerShellOpenRetryable(cause) {
	  return ["timeout", "network", "server"].includes(
	    readerShellFailureKind(cause)
	  );
	}
	function documentTopicId(document) {
	  const segments = document.location.pathname.split("/").filter(Boolean), topicIndex = segments.indexOf("t");
	  if (topicIndex < 0) return null;
	  const tail = segments.slice(topicIndex + 1), topicOffset = /^\d+$/.test(tail[0] ?? "") ? 0 : 1, topicId = Number(tail[topicOffset]);
	  return Number.isSafeInteger(topicId) && topicId > 0 ? topicId : null;
	}
	function createReaderLocalFontQuery(document) {
	  const browserWindow = document.defaultView;
	  if (!browserWindow?.queryLocalFonts) return;
	  let resolved = null, pending = null;
	  return async () => {
	    if (resolved) return resolved;
	    if (pending) return pending;
	    pending = browserWindow.queryLocalFonts().then((entries) => Object.freeze([...new Set(
	      entries.map((entry) => String(entry.family ?? "").trim()).filter(Boolean)
	    )].sort((left, right) => left.localeCompare(right)))).then((names) => (resolved = names, names));
	    try {
	      return await pending;
	    } finally {
	      pending = null;
	    }
	  };
	}
	class ReaderBrowserRuntime {
	  scope;
	  shell;
	  workspace;
	  permit;
	  data;
	  nativeAjax;
	  userNative;
	  users;
	  connectHistory;
	  creditAccount;
	  userEndorsements;
	  userActions;
	  userObservations;
	  userObservationView;
	  userCardView;
	  translationRequests;
	  translationFeature;
	  resourceRequests;
	  imageResources;
	  mediaPrefetch;
	  imageDownloads;
	  blobDownloads;
	  userMediaViewer;
	  assetCaches;
	  postReactions;
	  composerIsolation;
	  applicationCacheInvalidation;
	  composer;
	  controlTooltip;
	  actionSurfaces;
	  feedback;
	  recovery;
	  rateLimitNotice;
	  reportForm;
	  assignmentForm;
	  choiceForm;
	  topicEditForm;
	  selectSurface;
	  threadContextState;
	  history;
	  chronicle;
	  chronicleView;
	  unwantedTopics;
	  unwantedTopicView;
	  historyNavigation;
	  historyNavigationView;
	  historyPanelView;
	  notificationNative;
	  notificationRequests;
	  notificationActions;
	  notificationController;
	  notificationPanelView;
	  bookmarkNative;
	  bookmarkRequests;
	  bookmarkActions;
	  bookmarkController;
	  bookmarkPanelView;
	  #collectionActionEvents = new import_signal.Signal();
	  #chronicleRequestIds = /* @__PURE__ */ new Set();
	  topicFactory;
	  #performance;
	  #openRecoveryController = null;
	  #lastFailedRequest = null;
	  #challengeHref;
	  #openRetryDelay;
	  #loadingProgress;
	  #manualChallengeController;
	  #boostTargetHighlight;
	  #manualChallengePromise = null;
	  #destroyed = !1;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => this.#collectionActionEvents.clear()), this.#manualChallengeController = this.scope.abortController(
	      new DOMException("Reader runtime 已销毁", "AbortError")
	    ), this.shell = options.shell, this.workspace = options.workspace, this.#openRetryDelay = options.openRetryDelay ?? import_coordinated_request_client.abortableDelay, this.#loadingProgress = options.loadingProgress ?? null, this.#boostTargetHighlight = new import_reader_topic_scroll_adapter.ReaderBoostTargetHighlightController({
	      ...options.navigation?.readLifetimeMs ? { readLifetimeMs: options.navigation.readLifetimeMs } : {},
	      ...options.navigation?.prefersReducedMotion ? { prefersReducedMotion: options.navigation.prefersReducedMotion } : {},
	      ...options.navigation?.schedule ? { schedule: options.navigation.schedule } : {},
	      ...options.navigation?.cancel ? { cancel: options.navigation.cancel } : {},
	      parentScope: this.scope
	    }), this.#performance = options.performance ?? Object.freeze({
	      pageSize: options.topic.pageSize,
	      streamOverscanScreens: 1.5,
	      streamMaxMountedPostCount: 80,
	      nestedPrefetchScreens: 2.5,
	      requestMaxConcurrent: options.data.scheduler.maxConcurrent,
	      requestMinIntervalMs: options.permit.minIntervalMs,
	      requestRateTargetPercent: 85,
	      requestShortBudget: options.permit.shortBudget,
	      requestLongBudget: options.permit.longBudget
	    }), this.assetCaches = options.assetCacheStorage ? new import_browser_asset_cache.ReaderBrowserAssetCacheRepository(options.assetCacheStorage) : null;
	    let challengeHref = "";
	    try {
	      challengeHref = (0, import_browser_shared_request_permit.browserCloudflareChallengeHref)(
	        options.topic.origin ?? options.data.rateLimit.baseUrl ?? options.document.baseURI,
	        options.document.location?.href ?? options.document.baseURI
	      );
	    } catch {
	    }
	    this.#challengeHref = challengeHref;
	    const reportTopicFeature = (topicId, feature, cause) => {
	      try {
	        options.onTopicFeatureError?.(Object.freeze({
	          topicId,
	          feature,
	          cause
	        }));
	      } catch {
	      }
	    };
	    try {
	      const nativeUserCatalog = (0, import_native_host_api.discourseNativeUnwantedTopicRuleCatalog)(options.host);
	      this.selectSurface = new import_reader_select_surface.ReaderSelectSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        parentScope: this.scope
	      }), this.actionSurfaces = new import_reader_action_surface_coordinator.ReaderActionSurfaceCoordinator({
	        parentScope: this.scope
	      }), this.controlTooltip = new import_reader_control_tooltip.ReaderControlTooltip({
	        document: options.document,
	        surfaceHost: this.shell.view.surfaceHost,
	        ...options.share ? { copyText: (value) => options.share.copyText(value) } : {},
	        parentScope: this.scope
	      });
	      const openNative = this.shell.view.root.querySelector("a.ldp-open"), browserWindow = options.document.defaultView;
	      if (openNative && browserWindow) {
	        const openNativeTopic = (event) => {
	          const pointer = event;
	          event.type === "click" && pointer.button !== 0 || event.type === "auxclick" && pointer.button !== 1 || !openNative.href || openNative.hidden || !(0, import_reader_native_topic_route.openReaderNativeTopicTab)(browserWindow, openNative.href) || (event.preventDefault(), event.stopPropagation());
	        };
	        this.scope.listen(openNative, "click", openNativeTopic), this.scope.listen(openNative, "auxclick", openNativeTopic);
	      }
	      if (this.feedback = new import_reader_feedback_surface.ReaderFeedbackSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        coordinator: this.actionSurfaces,
	        ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	        parentScope: this.scope
	      }), this.recovery = new import_reader_shell_recovery_view.ReaderShellRecoveryView({
	        document: options.document,
	        host: this.shell.view.topicHost,
	        onRetry: async () => {
	          const request = this.#lastFailedRequest;
	          if (!request) return !1;
	          await this.data.client.resetRateLimits();
	          const result = await this.openTarget({
	            ...request,
	            forceRefresh: !0
	          });
	          return result.topic.status === "opened" || result.topic.status === "reused";
	        },
	        onClose: async () => {
	          await this.close();
	        },
	        parentScope: this.scope
	      }), this.reportForm = new import_reader_report_form_surface.ReaderReportFormSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        coordinator: this.actionSurfaces,
	        ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	        parentScope: this.scope
	      }), this.assignmentForm = new import_reader_assignment_form_surface.ReaderAssignmentFormSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        users: nativeUserCatalog,
	        coordinator: this.actionSurfaces,
	        ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	        parentScope: this.scope
	      }), this.choiceForm = new import_reader_choice_form_surface.ReaderChoiceFormSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        coordinator: this.actionSurfaces,
	        ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	        parentScope: this.scope
	      }), this.topicEditForm = new import_reader_topic_edit_form_surface.ReaderTopicEditFormSurface({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	        parentScope: this.scope
	      }), this.threadContextState = new import_reader_topic_context_state.ReaderTopicContextStateRepository({
	        storage: options.threadContextStorage ?? (0, import_reader_topic_context_state.readerTopicContextWebStorage)(options.storage),
	        authScope: options.topic.authScope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "thread-context",
	          cause
	        )
	      }), this.threadContextState.load(), options.lightbox && options.openTopicImage)
	        throw new Error(
	          "标准 Lightbox 与自定义 openTopicImage 只能配置一个"
	        );
	      const challengeOrigin = String(
	        options.topic.origin ?? options.data.rateLimit.baseUrl ?? options.document.location?.origin ?? ""
	      ), challengeWindow = options.document.defaultView;
	      this.nativeAjax = new import_discourse_native_read_transport.BrowserDiscourseNativeAjaxPort(
	        options.host,
	        options.topic.origin === void 0 ? {} : { origin: options.topic.origin }
	      );
	      let challengeRequestObserver = null;
	      this.permit = new import_browser_shared_request_permit.BrowserSharedRequestPermit({
	        ...options.permit,
	        storage: options.storage,
	        sourceId: options.sourceId,
	        locks: options.locks ?? null,
	        storageEvents: options.storageEvents ?? null,
	        ...options.broadcastChannelFactory === void 0 ? {} : { broadcastChannelFactory: options.broadcastChannelFactory },
	        .../^https?:\/\//i.test(challengeOrigin) && typeof challengeWindow?.open == "function" ? {
	          challenge: {
	            origin: challengeOrigin,
	            redirectHref: options.document.location?.href ?? options.document.baseURI,
	            verify: async (signal) => {
	              const observer = challengeRequestObserver, observationId = observer?.begin({
	                href: "/session/current.json?_=cache-bust",
	                method: "GET",
	                transport: "xmlhttprequest",
	                source: "reader",
	                priority: "critical",
	                logicalId: "CF-probe",
	                profile: "challenge-probe",
	                namespace: "cloudflare-session",
	                lane: "control",
	                cacheMode: "no-store",
	                max429Retries: 0,
	                maxChallengeRetries: 0,
	                blockOnCloudflareChallenge: !1,
	                suppressAfterChallengeWait: !0,
	                droppable: !1,
	                callSite: "cloudflare-challenge / session-probe"
	              }) ?? null;
	              let response;
	              try {
	                response = await this.nativeAjax.request({
	                  path: "/session/current.json",
	                  method: "GET",
	                  signal,
	                  noStore: !0
	                });
	              } catch (error) {
	                if (observationId !== null) {
	                  const decision = signal.aborted ? "challenge-probe-cancelled" : "challenge-probe-failed";
	                  observer?.finish(observationId, {
	                    error: signal.aborted ? "AbortError" : "request-failed",
	                    decision
	                  }) === !1 && observer?.update(observationId, { decision });
	                }
	                throw error;
	              }
	              if (observationId !== null) {
	                const decision = response.cloudflareMitigated === !0 ? "challenge-probe-blocked" : response.status === 429 ? "challenge-probe-rate-limited-pass" : "challenge-probe-passed";
	                observer?.finish(observationId, {
	                  status: response.status,
	                  cloudflareMitigated: response.cloudflareMitigated === !0,
	                  retryAfter: String(response.retryAfter ?? ""),
	                  rateLimitCode: String(response.rateLimitCode ?? ""),
	                  serverLimit: String(response.serverLimit ?? ""),
	                  serverRemaining: String(response.serverRemaining ?? ""),
	                  serverReset: String(response.serverReset ?? ""),
	                  decision
	                }) === !1 && observer?.update(observationId, { decision });
	              }
	              return response.status >= 100 && response.cloudflareMitigated !== !0;
	            },
	            screen: challengeWindow.screen,
	            open: (url, name, features) => challengeWindow.open(
	              url,
	              name,
	              features
	            )
	          }
	        } : {},
	        parentScope: this.scope
	      });
	      const rateLimitNotice = this.shell.view.root.querySelector(
	        ".ldp-rate-limit-notice"
	      ), rateLimitDetail = rateLimitNotice?.querySelector(
	        ".ldp-rate-limit-detail"
	      ), rateLimitChallenge = rateLimitNotice?.querySelector(
	        ".ldp-rate-limit-challenge"
	      );
	      if (!rateLimitNotice || !rateLimitDetail || !rateLimitChallenge)
	        throw new Error("Reader Shell 缺少 429 状态投影锚点");
	      this.rateLimitNotice = new import_reader_rate_limit_notice.ReaderRateLimitNotice({
	        document: options.document,
	        elements: {
	          root: rateLimitNotice,
	          detail: rateLimitDetail,
	          challenge: rateLimitChallenge
	        },
	        challengeHref: this.#challengeHref,
	        snapshot: () => this.permit.snapshot(),
	        parentScope: this.scope
	      }), this.scope.listen(this.shell.view.root, "click", (event) => {
	        const target = event.target, anchor = typeof target?.closest == "function" ? target.closest(
	          "a.ldp-rate-limit-challenge,a.ldp-error-challenge"
	        ) : null;
	        !anchor?.href || !this.#challengeHref || (event.preventDefault(), event.stopPropagation(), this.#openManualCloudflareChallenge(anchor.href));
	      }), this.data = new import_reader_data_runtime.ReaderDataRuntime({
	        ...options.data,
	        permit: this.permit,
	        storage: options.storage,
	        sourceId: options.sourceId,
	        locks: options.locks ?? null,
	        indexedDb: options.indexedDb ?? null,
	        ...options.broadcastChannelFactory === void 0 ? {} : { broadcastChannelFactory: options.broadcastChannelFactory },
	        parentScope: this.scope
	      }), challengeRequestObserver = this.data.requests, this.permit.reconcileCloudflareChallenge().then(() => this.rateLimitNotice.refresh()).catch(() => {
	      }), this.composerIsolation = new import_native_composer.DiscourseComposerHostIsolation({
	        host: options.host,
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "post-action",
	          cause
	        )
	      }), this.applicationCacheInvalidation = new import_discourse_application_cache_invalidation.DiscourseApplicationCacheInvalidationCoordinator({
	        host: options.host,
	        composerEvents: this.composerIsolation,
	        cache: this.data.responses,
	        currentTopicId: () => this.shell.activeTopicId === null ? documentTopicId(options.document) : Number(this.shell.activeTopicId),
	        onPostChanged: (post) => {
	          this.shell.activeValue?.services.live.ingestPostDelta(post);
	        },
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "post-action",
	          cause
	        )
	      });
	      const nativeActions = new import_discourse_action_transport.BrowserDiscourseNativeActionPort(
	        options.host,
	        this.nativeAjax,
	        this.composerIsolation
	      ), nativeReads = new import_discourse_native_read_transport.BrowserDiscourseNativeReadTransport(
	        this.nativeAjax
	      ), actionDescriptors = new import_discourse_action_descriptors.DiscourseActionDescriptors();
	      this.userNative = new import_discourse_native_user_port.BrowserDiscourseNativeUserPort(options.host, {
	        readTransport: nativeReads,
	        ...options.topic.origin === void 0 ? {} : { basePath: options.topic.origin },
	        categoryExperts: options.document.location?.hostname === "linux.do"
	      }), this.userEndorsements = new import_reader_user_endorsement_adapter.ReaderUserEndorsementAdapter({
	        gateway: this.data.gateway,
	        transport: nativeReads,
	        authScope: options.topic.authScope
	      });
	      const connect = options.connect ? new import_reader_connect_trust_adapter.ReaderConnectTrustAdapter({
	        gateway: this.data.gateway,
	        http: options.connect.http,
	        authScope: options.topic.authScope,
	        document: options.document
	      }) : null;
	      this.connectHistory = connect ? new import_reader_connect_trust_adapter.ReaderConnectTrustHistoryAdapter({
	        gateway: this.data.gateway,
	        ajax: this.nativeAjax,
	        storage: options.storage,
	        confirmations: this.data.readCoordination,
	        authScope: options.topic.authScope
	      }) : null, this.connectHistory && this.scope.add(this.data.readCoordination.subscribeConfirmations(
	        (confirmation) => {
	          this.connectHistory?.recordReadConfirmation(confirmation);
	        }
	      )), this.creditAccount = options.credit ? new import_reader_credit_account_adapter.ReaderCreditAccountAdapter({
	        gateway: this.data.gateway,
	        http: options.credit.http,
	        authScope: options.topic.authScope,
	        ...options.credit.storage ? { storage: options.credit.storage } : {}
	      }) : null;
	      const credit = this.creditAccount;
	      this.users = new import_reader_user_domain_session.ReaderUserDomainSession({
	        gateway: this.data.gateway,
	        native: this.userNative,
	        authScope: options.topic.authScope,
	        ...options.searchForms === void 0 ? {} : { searchForms: options.searchForms },
	        ...connect ? { connect } : {},
	        ...credit ? { credit } : {},
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      });
	      const userAbort = this.scope.abortController(
	        new Error("Reader 用户 action scope 已销毁")
	      ), userMutation = new import_action_request_adapter.ActionRequestAdapter({
	        gateway: this.data.gateway,
	        nativeActions,
	        authScope: options.topic.authScope,
	        signal: userAbort.signal
	      });
	      this.userActions = new import_post_action_controller.PostActionController({
	        mutation: userMutation,
	        cache: this.data.responses,
	        scope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      });
	      const userCommands = new import_user_action_feature_commands.UserActionFeatureCommands({
	        state: this.users
	      }), setUserNotificationLevel = async (username, level, expiringAt) => {
	        const binding = this.userNative.actionBinding(username);
	        await this.userActions.dispatch(
	          userCommands.notificationLevel(
	            username,
	            level,
	            actionDescriptors.userNotificationLevel({
	              username,
	              user: binding.user,
	              level,
	              expiringAt: expiringAt ?? null,
	              actingUser: binding.actingUser
	            })
	          )
	        );
	      };
	      this.composer = new import_native_composer.DiscourseComposerCoordinator({
	        host: options.host,
	        document: options.document,
	        isolation: this.composerIsolation,
	        parentScope: this.scope,
	        onError: (cause) => {
	          try {
	            options.lightbox?.onError?.(cause);
	          } catch {
	          }
	        }
	      }), this.composer.installSubmitGuard({
	        document: options.document,
	        parentScope: this.scope
	      });
	      const userObservationPresentation = (0, import_native_host_api.discourseNativeTopicPresentation)(options.host), userObservationPages = new import_reader_user_observation_page_repository.ReaderUserObservationPageRepository(
	        this.data.responses,
	        options.topic.authScope,
	        this.data.cacheCoordination
	      );
	      if (this.userObservations = new import_reader_user_observation_session.ReaderUserObservationSession({
	        requests: new import_discourse_user_observation_adapter.DiscourseUserObservationAdapter({
	          gateway: this.data.gateway,
	          ajax: this.nativeAjax,
	          authScope: options.topic.authScope,
	          cache: {
	            kind: "discourse-user-observation",
	            tags: ["users", "user-observation"],
	            freshForMs: 10 * 6e4,
	            retainForMs: options.topic.caches.posts.retainForMs,
	            persist: !0
	          },
	          categoryName: (categoryId) => userObservationPresentation.categoryName?.(categoryId) ?? ""
	        }),
	        storage: options.storage,
	        pages: userObservationPages,
	        authScope: options.topic.authScope,
	        historyCoordination: this.data.cacheCoordination,
	        historyCoordinationKey: `reader-user-observation-history:v1:${options.topic.authScope}`,
	        requestResume: (cause) => this.data.client.requestResume(cause),
	        notify: (message) => this.feedback.show(message),
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      }), this.scope.add(this.data.cacheCoordination.subscribeInvalidation((query) => {
	        this.users.applyExternalCacheInvalidation(query), this.userObservations.applyExternalCacheInvalidation(query);
	      })), this.userObservationView = new import_reader_user_observation_view.ReaderUserObservationView({
	        document: options.document,
	        mount: this.shell.view.surfaceHost,
	        session: this.userObservations,
	        storage: options.storage,
	        pages: userObservationPages,
	        avatarSource: (template, size) => this.userNative.avatarSource(template, size),
	        emojiSource: (id) => (0, import_native_host_api.discourseNativeEmojiUrl)(options.host, id),
	        openTarget: async (topicId, postNumber, record) => {
	          const boostId = record.kind === "boost" ? Number(record.identity.match(/^boost:(\d+)$/)?.[1]) : 0, result = await this.openTarget({
	            topicId,
	            postNumber,
	            ...Number.isSafeInteger(boostId) && boostId > 0 ? { boostId } : {},
	            source: "link",
	            highlight: !0
	          });
	          return (result.topic.status === "opened" || result.topic.status === "reused") && result.navigation?.status === "revealed";
	        },
	        openChallenge: (username) => {
	          this.#openManualCloudflareChallenge(
	            this.#challengeHref,
	            username
	          );
	        },
	        notify: (message) => this.feedback.show(message),
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      }), this.chronicle = new import_reader_chronicle_repository.ReaderChronicleRepository({
	        storage: options.storage,
	        authScope: options.topic.authScope
	      }), this.chronicle.load(), this.chronicleView = new import_reader_chronicle_view.ReaderChronicleView({
	        document: options.document,
	        mount: this.shell.view.surfaceHost,
	        chronicle: this.chronicle,
	        storage: options.storage,
	        openTarget: async (topicId, postNumber, record) => {
	          const result = await this.openTarget({
	            topicId,
	            postNumber,
	            source: "chronicle",
	            highlight: !0,
	            ...record.kind === "reply" ? {
	              cachedOnly: !0,
	              revealAsFloor: !0,
	              localArchive: Object.freeze({
	                status: 404,
	                confirmedAt: record.lastObservedAt,
	                requestPath: record.requestPath
	              })
	            } : {}
	          });
	          return (result.topic.status === "opened" || result.topic.status === "reused") && result.navigation?.status === "revealed";
	        },
	        notify: (message) => this.feedback.show(message),
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "history",
	          cause
	        )
	      }), this.unwantedTopics = new import_reader_unwanted_topic_repository.ReaderUnwantedTopicRepository({
	        storage: options.storage,
	        authScope: options.topic.authScope
	      }), this.unwantedTopics.load(), this.unwantedTopicView = new import_reader_unwanted_topic_view.ReaderUnwantedTopicView({
	        document: options.document,
	        mount: this.shell.view.surfaceHost,
	        topics: this.unwantedTopics,
	        ...options.unwantedTopicFilter ? {
	          filterPreferences: options.unwantedTopicFilter,
	          filterCatalog: nativeUserCatalog
	        } : {},
	        storage: options.storage,
	        openTarget: async (record) => {
	          const result = await this.openTarget({
	            topicId: record.topicId,
	            postNumber: 1,
	            source: "link",
	            highlight: !0
	          });
	          return result.topic.status === "opened" || result.topic.status === "reused";
	        },
	        notify: (message) => this.feedback.show(message),
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "history",
	          cause
	        )
	      }), this.userObservations.resume({ allowNetwork: !1 }), this.userCardView = new import_reader_user_card_view.ReaderUserCardView({
	        document: options.document,
	        root: this.shell.view.surfaceHost,
	        hoverDelegates: Object.freeze([Object.freeze({
	          root: options.document,
	          selector: hostTopicUserCardSelector,
	          capture: !0
	        })]),
	        session: this.users,
	        userHref: (username) => this.userNative.requestIdentity(username),
	        avatarSource: (template, size) => this.userNative.avatarSource(template, size),
	        recoverAvatarSource: (source) => this.#recoverAvatarSource(source),
	        toggleFollow: async (username, followed) => {
	          await this.userActions.dispatch(userCommands.follow(
	            username,
	            followed,
	            actionDescriptors.userFollowToggle({ username, followed }),
	            (0, import_native_host_api.discourseNativeCurrentUsername)(options.host)
	          ));
	        },
	        openMessage: async (username) => {
	          await this.composer.openPrivateMessage(username);
	        },
	        observeUser: (profile) => this.userObservationView.observe(profile),
	        isObserved: (username) => this.userObservations.isObserved(username),
	        setNotificationLevel: async (username, level, expiringAt) => {
	          await setUserNotificationLevel(username, level, expiringAt), this.feedback.show(
	            level === "normal" ? `已恢复 @${username} 的常规通知` : level === "mute" ? `已将 @${username} 设为免打扰` : `已忽略 @${username}`
	          );
	        },
	        ignoreUser: (username) => this.choiceForm.open({
	          title: `忽略 @${username}`,
	          intro: "Discourse 要求为“忽略”设置截止时间;到期后会自动恢复为常规。",
	          fieldLabel: "忽略期限",
	          mode: "select",
	          options: Object.freeze([
	            Object.freeze({ value: "1", label: "1 天" }),
	            Object.freeze({ value: "7", label: "1 周" }),
	            Object.freeze({ value: "30", label: "1 个月", selected: !0 }),
	            Object.freeze({ value: "120", label: "4 个月" }),
	            Object.freeze({ value: "365", label: "1 年" })
	          ]),
	          submitLabel: "确认忽略",
	          emptySelectionError: "请选择有效的忽略期限",
	          submit: async ([value]) => {
	            const days = Number(value);
	            if (![1, 7, 30, 120, 365].includes(days))
	              throw new Error("请选择有效的忽略期限");
	            return await setUserNotificationLevel(
	              username,
	              "ignore",
	              new Date(Date.now() + days * 864e5).toISOString()
	            ), `已忽略 @${username}`;
	          }
	        }),
	        endorseUser: async (profile) => {
	          const username = profile.identity.username, catalog = await this.userEndorsements.load(
	            username,
	            userAbort.signal
	          );
	          if (!catalog.categories.length)
	            throw new Error("当前没有可认可的类别");
	          const existingIds = new Set(
	            (profile.categoryExperts.endorsements ?? []).map((item) => item.categoryId)
	          );
	          return this.choiceForm.open({
	            title: `认可 @${username}`,
	            intro: catalog.remainingEndorsements === null ? "选择要认可的专家类别。" : `今天还可新增 ${catalog.remainingEndorsements} 次认可。`,
	            mode: "multiple",
	            options: Object.freeze(catalog.categories.map((category) => {
	              const existing = existingIds.has(category.id);
	              return Object.freeze({
	                value: String(category.id),
	                label: category.name,
	                selected: existing,
	                disabled: existing,
	                description: existing ? "已经认可" : "选择后将认可该用户为此类别的专家"
	              });
	            })),
	            submitLabel: "确认认可",
	            emptySelectionError: "请选择一个尚未认可的类别",
	            submit: async (values) => {
	              if (catalog.remainingEndorsements !== null && catalog.remainingEndorsements < 1)
	                throw new Error("今天的认可次数已用完");
	              const addedIds = values.map(Number).filter((categoryId) => Number.isSafeInteger(categoryId) && categoryId > 0 && !existingIds.has(categoryId));
	              if (!addedIds.length)
	                throw new Error("请选择一个尚未认可的类别");
	              return await this.userActions.dispatch(userCommands.endorse(
	                username,
	                actionDescriptors.categoryExpertEndorse({
	                  username,
	                  categoryIds: Object.freeze([
	                    .../* @__PURE__ */ new Set([...existingIds, ...addedIds])
	                  ])
	                })
	              )), "认可已提交";
	            },
	            signal: userAbort.signal
	          }).catch((cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "user",
	            cause
	          )), !0;
	        },
	        ...options.lightbox ? {
	          openMedia: (items, initialIndex, anchor, profile, returnFocus) => {
	            const viewer = this.userMediaViewer, descriptor = items[initialIndex];
	            if (!viewer || !descriptor)
	              throw new Error(
	                "用户媒体紧凑查看器尚未装配"
	              );
	            const item = Object.freeze({
	              key: `user:${descriptor.kind}:${descriptor.src}`,
	              previewSrc: descriptor.src,
	              originalSrc: descriptor.originalSrc ?? descriptor.src,
	              alt: descriptor.alt,
	              topicId: this.shell.activeTopicId ?? 1,
	              sourcePostNumber: 1,
	              imageOrder: initialIndex
	            });
	            viewer.open({
	              item,
	              kind: descriptor.kind === "avatar" ? "avatar" : "background",
	              anchor,
	              returnFocus: () => returnFocus ?? anchor,
	              outsideSafeSurface: anchor,
	              flair: descriptor.kind === "avatar" ? profile.flair : null,
	              ...this.imageDownloads ? {
	                onDownload: async () => {
	                  await this.imageDownloads.download(
	                    item,
	                    initialIndex,
	                    { original: !0 }
	                  );
	                }
	              } : {},
	              onDismiss: () => this.userCardView.close()
	            });
	          }
	        } : {},
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      }), this.#applyPerformanceInfrastructure(), this.translationRequests = options.translation ? new import_translation_request_adapter.TranslationRequestAdapter({
	        ...options.translation,
	        gateway: this.data.gateway
	      }) : null, this.scope.add(() => this.translationRequests?.destroy()), options.translationView === !1)
	        this.translationFeature = null;
	      else if (this.translationRequests) {
	        const {
	          buttonHost,
	          initialMode,
	          renderIcon: translationRenderIcon,
	          onError: translationOnError,
	          ...translationViewOptions
	        } = options.translationView ?? {}, resolvedButtonHost = buttonHost ?? this.shell.view.root.querySelector(
	          ".ldp-head-btns"
	        );
	        if (!resolvedButtonHost)
	          throw new Error("翻译 View 缺少稳定 Shell headerActions");
	        this.translationFeature = new import_reader_translation_feature.ReaderTranslationFeature({
	          ...translationViewOptions,
	          document: options.document,
	          translator: this.translationRequests,
	          buttonHost: resolvedButtonHost,
	          surfaces: () => {
	            const discussion = this.shell.view.modal.querySelector(
	              ":scope > .ldp-descendant-replies-layer"
	            );
	            return Object.freeze([
	              this.shell.view.body,
	              ...discussion ? [discussion] : []
	            ]);
	          },
	          ...translationRenderIcon ? { renderIcon: translationRenderIcon } : options.renderIcon ? {
	            renderIcon: (document) => options.renderIcon("languages", document)
	          } : {},
	          initialMode: initialMode ?? "original",
	          parentScope: this.scope,
	          notify: (message) => this.feedback.show(message),
	          onError: (cause) => {
	            try {
	              translationOnError?.(cause);
	            } catch {
	            }
	            reportTopicFeature(
	              this.shell.activeTopicId ?? 0,
	              "translation",
	              cause
	            );
	          }
	        });
	      } else {
	        if (options.translationView)
	          throw new Error(
	            "翻译 View 需要先配置唯一 TranslationRequestAdapter"
	          );
	        this.translationFeature = null;
	      }
	      if (options.resources) {
	        const {
	          objectUrls,
	          maxObjectUrls,
	          downloadMount,
	          downloadUrlRevokeAfterMs,
	          ...resourceOptions
	        } = options.resources;
	        this.resourceRequests = new import_public_resource_request_adapter.PublicResourceRequestAdapter({
	          ...resourceOptions,
	          gateway: this.data.gateway
	        }), this.imageResources = new import_reader_image_resource_service.ReaderImageResourceService({
	          resources: this.resourceRequests,
	          objectUrls,
	          ...maxObjectUrls === void 0 ? {} : { maxObjectUrls },
	          parentScope: this.scope
	        }), this.mediaPrefetch = new import_reader_media_prefetch_service.ReaderMediaPrefetchService({
	          document: options.document,
	          baseUrl: resourceOptions.baseUrl,
	          resources: this.resourceRequests,
	          concurrency: 2
	        }), this.blobDownloads = new import_reader_image_download_service.BrowserBlobDownloadPort({
	          document: options.document,
	          mount: downloadMount ?? options.document.body ?? options.document.documentElement,
	          objectUrls,
	          ...downloadUrlRevokeAfterMs === void 0 ? {} : { revokeAfterMs: downloadUrlRevokeAfterMs },
	          parentScope: this.scope
	        }), this.imageDownloads = new import_reader_image_download_service.ReaderImageDownloadService({
	          resources: this.imageResources,
	          downloads: this.blobDownloads
	        });
	      } else
	        this.resourceRequests = null, this.imageResources = null, this.mediaPrefetch = null, this.imageDownloads = null, this.blobDownloads = null;
	      this.userMediaViewer = options.lightbox ? new import_reader_compact_image_viewer.ReaderCompactImageViewer({
	        document: options.document,
	        mount: typeof options.lightbox.mount == "function" ? options.lightbox.mount() : options.lightbox.mount,
	        ...this.imageResources || options.lightbox.originalSources ? {
	          originalSources: this.imageResources ?? options.lightbox.originalSources
	        } : {},
	        ...options.lightbox.frameScheduler ? { frameScheduler: options.lightbox.frameScheduler } : {},
	        notify: (message) => this.feedback.show(message),
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "user",
	          cause
	        )
	      }) : null;
	      const {
	        onReady: topicReady,
	        createDomOptions,
	        ...topicFactoryOptions
	      } = options.topicFactory, topicBaseUrl = options.topic.origin ?? options.data.rateLimit.baseUrl ?? options.document.baseURI, nativeRelativeTime = (0, import_native_host_api.discourseNativeRelativeTimeFormatter)(options.host), nativeExactTime = (0, import_native_host_api.discourseNativeExactTimeFormatter)(options.host), nativeTopicPresentation = (0, import_native_host_api.discourseNativeTopicPresentation)(options.host), nativeTopicEditCatalog = (0, import_native_host_api.discourseNativeTopicEditCatalog)(options.host), nativePresence = new import_native_presence.BrowserDiscoursePresencePort(options.host), nativeTopicLinks = (0, import_native_host_api.discourseNativeTopicLinks)(options.host, topicBaseUrl), topicSummaryTransport = (() => {
	        try {
	          return new URL(
	            topicBaseUrl,
	            options.document.baseURI
	          ).hostname.toLocaleLowerCase() === "linux.do" ? new import_discourse_native_read_transport.BrowserDiscourseNativeMutationTransport(this.nativeAjax) : null;
	        } catch {
	          return null;
	        }
	      })(), nativeFlagCatalog = (0, import_native_host_api.discourseNativeFlagCatalog)(options.host), nativeEmojiMenu = (0, import_native_host_api.discourseNativeEmojiMenu)(options.host), nativeAdminMenu = (0, import_native_host_api.discourseNativePostAdminMenu)(options.host, {
	        computePosition: (anchor, content) => {
	          (0, import_reader_native_post_admin_menu.positionReaderNativePostAdminMenu)({
	            document: options.document,
	            reader: this.shell.view.modal,
	            anchor,
	            content
	          });
	        }
	      }), nativePostModels = new import_native_post_model_factory.DiscourseNativePostModelFactory(options.host), nativeBookmarkForm = new import_native_host_api.BrowserDiscourseNativeBookmarkForm(options.host);
	      this.postReactions = new import_reader_post_action_feature.DiscoursePostReactionCatalog(
	        nativePostModels
	      );
	      const postReactions = this.postReactions, topicFeatures = /* @__PURE__ */ new WeakMap(), topicDoms = /* @__PURE__ */ new WeakMap(), topicContextSurfaces = /* @__PURE__ */ new WeakMap(), topicHeaders = /* @__PURE__ */ new WeakMap(), topicHeaderViews = /* @__PURE__ */ new WeakMap(), topicOnlyOpControllers = /* @__PURE__ */ new WeakMap(), topicNavigations = /* @__PURE__ */ new WeakMap(), topicFlows = /* @__PURE__ */ new WeakMap(), topicTimelines = /* @__PURE__ */ new WeakMap(), topicTimelineViews = /* @__PURE__ */ new WeakMap(), topicLiveNavigations = /* @__PURE__ */ new WeakMap(), topicLiveNavigationViews = /* @__PURE__ */ new WeakMap(), topicSummaryPreviews = /* @__PURE__ */ new WeakMap(), authenticatedCollectionScope = options.topic.authScope.startsWith("account:");
	      if (options.notifications === !1 || !authenticatedCollectionScope)
	        this.notificationNative = null, this.notificationRequests = null, this.notificationActions = null, this.notificationController = null, this.notificationPanelView = null;
	      else {
	        const notificationOptions = options.notifications ?? {}, notificationAbort = this.scope.abortController(
	          new Error("Reader 通知 application scope 已销毁")
	        );
	        this.notificationNative = new import_native_host_api.BrowserDiscourseNotificationNativeState(options.host), this.notificationRequests = new import_discourse_notification_adapter.DiscourseNotificationRequestAdapter({
	          gateway: this.data.gateway,
	          ajax: this.nativeAjax,
	          native: this.notificationNative,
	          authScope: options.topic.authScope,
	          signal: notificationAbort.signal,
	          ...options.topic.basePath === void 0 ? {} : { basePath: options.topic.basePath },
	          replyExpansionCache: {
	            kind: "discourse-topic-posts",
	            tags: ["notifications"],
	            ...options.topic.caches.posts
	          },
	          categoryNameFor: (categoryId) => userObservationPresentation.categoryName?.(categoryId) ?? ""
	        });
	        const notificationMutation = new import_action_request_adapter.ActionRequestAdapter({
	          gateway: this.data.gateway,
	          nativeActions,
	          authScope: options.topic.authScope,
	          signal: notificationAbort.signal
	        });
	        this.notificationActions = new import_post_action_controller.PostActionController({
	          mutation: notificationMutation,
	          cache: this.data.responses,
	          scope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "notification",
	            cause
	          )
	        }), this.notificationController = new import_reader_notification_controller.ReaderNotificationController({
	          requests: this.notificationRequests,
	          projection: new import_reader_collection_page_repository.ReaderCollectionPageRepository({
	            responses: this.data.responses,
	            authScope: options.topic.authScope,
	            namespace: "notifications",
	            kind: "reader-notification-projection",
	            tags: ["notification-projection"],
	            normalizeRecord: import_reader_notification_model.normalizeStoredReaderNotification,
	            sortRecords: import_reader_notification_model.sortReaderNotifications,
	            pageSize: 60,
	            retainForMs: 4320 * 60 * 6e4,
	            permanent: !0,
	            coordination: this.data.cacheCoordination
	          }),
	          native: this.notificationNative,
	          actions: this.notificationActions,
	          cache: this.data.responses,
	          target: {
	            openTarget: async (request) => {
	              const result = await this.openTarget(request);
	              return (result.topic.status === "opened" || result.topic.status === "reused") && result.navigation?.status === "revealed";
	            }
	          },
	          ...notificationOptions.maxCachedPages === void 0 ? {} : {
	            maxCachedPages: notificationOptions.maxCachedPages
	          },
	          ...notificationOptions.liveRefreshDelayMs === void 0 ? {} : {
	            liveRefreshDelayMs: notificationOptions.liveRefreshDelayMs
	          },
	          backgroundWarmDelayMs: notificationOptions.backgroundWarmDelayMs ?? 1800,
	          ...notificationOptions.openRevalidateMs === void 0 ? {} : {
	            openRevalidateMs: notificationOptions.openRevalidateMs
	          },
	          ...notificationOptions.nativePollIntervalMs === void 0 ? {} : {
	            nativePollIntervalMs: notificationOptions.nativePollIntervalMs
	          },
	          ...notificationOptions.syntheticPollIntervalMs === void 0 ? {} : {
	            syntheticPollIntervalMs: notificationOptions.syntheticPollIntervalMs
	          },
	          ...notificationOptions.historyStepDelayMs === void 0 ? {} : {
	            historyStepDelayMs: notificationOptions.historyStepDelayMs
	          },
	          ...notificationOptions.historyRetryDelayMs === void 0 ? {} : {
	            historyRetryDelayMs: notificationOptions.historyRetryDelayMs
	          },
	          visibleHistoryConcurrency: notificationOptions.visibleHistoryConcurrency ?? 3,
	          historyCoordination: this.data.cacheCoordination,
	          historyCoordinationKey: `reader-notification-history:v1:${options.topic.authScope}`,
	          activity: {
	            visible: () => options.document.visibilityState !== "hidden",
	            subscribe: (listener) => {
	              const view = options.document.defaultView, onActivity = () => listener();
	              return options.document.addEventListener(
	                "visibilitychange",
	                onActivity
	              ), view?.addEventListener("focus", onActivity), view?.addEventListener("online", onActivity), () => {
	                options.document.removeEventListener(
	                  "visibilitychange",
	                  onActivity
	                ), view?.removeEventListener("focus", onActivity), view?.removeEventListener("online", onActivity);
	              };
	            }
	          },
	          ...notificationOptions.schedule === void 0 ? {} : { schedule: notificationOptions.schedule },
	          ...notificationOptions.cancel === void 0 ? {} : { cancel: notificationOptions.cancel },
	          ...(notificationOptions.searchForms ?? options.searchForms) === void 0 ? {} : {
	            searchForms: notificationOptions.searchForms ?? options.searchForms
	          },
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "notification",
	            cause
	          )
	        }), this.notificationPanelView = new import_reader_notification_panel_view.ReaderNotificationPanelView({
	          ...notificationOptions,
	          ...notificationOptions.renderIcon ? {} : options.renderIcon ? { renderIcon: options.renderIcon } : {},
	          document: options.document,
	          mount: this.shell.view.surfaceHost,
	          storage: options.storage,
	          controller: this.notificationController,
	          elements: readerNotificationPanelElements(
	            this.shell.view.root
	          ),
	          baseUrl: topicBaseUrl,
	          relativeTime: nativeRelativeTime,
	          emojiSource: notificationOptions.emojiSource ?? ((id) => (0, import_native_host_api.discourseNativeEmojiUrl)(options.host, id)),
	          archiveMarker: (topicId, postNumber) => this.#historyArchiveMarker(topicId, postNumber),
	          notify: (message) => this.feedback.show(message),
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "notification",
	            cause
	          )
	        });
	      }
	      if (options.bookmarks === !1 || !authenticatedCollectionScope)
	        this.bookmarkNative = null, this.bookmarkRequests = null, this.bookmarkActions = null, this.bookmarkController = null, this.bookmarkPanelView = null;
	      else {
	        const bookmarkOptions = options.bookmarks ?? {}, bookmarkAbort = this.scope.abortController(
	          new Error("Reader 收藏 application scope 已销毁")
	        );
	        this.bookmarkNative = new import_native_host_api.BrowserDiscourseBookmarkNativeState(options.host), this.bookmarkRequests = new import_discourse_bookmark_adapter.DiscourseBookmarkRequestAdapter({
	          gateway: this.data.gateway,
	          ajax: this.nativeAjax,
	          native: this.bookmarkNative,
	          authScope: options.topic.authScope,
	          signal: bookmarkAbort.signal,
	          cache: {
	            kind: "discourse-bookmark-collection",
	            tags: [
	              "bookmarks",
	              "reactions-given",
	              "boosts-given",
	              "replied-topics"
	            ],
	            freshForMs: 30 * 6e4,
	            retainForMs: options.topic.caches.posts.retainForMs,
	            persist: !0
	          },
	          categoryNameFor: (categoryId) => userObservationPresentation.categoryName?.(categoryId) ?? ""
	        });
	        const bookmarkMutation = new import_action_request_adapter.ActionRequestAdapter({
	          gateway: this.data.gateway,
	          nativeActions,
	          authScope: options.topic.authScope,
	          signal: bookmarkAbort.signal
	        });
	        this.bookmarkActions = new import_post_action_controller.PostActionController({
	          mutation: bookmarkMutation,
	          cache: this.data.responses,
	          scope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "bookmark",
	            cause
	          )
	        }), this.bookmarkController = new import_reader_bookmark_controller.ReaderBookmarkController({
	          requests: this.bookmarkRequests,
	          projection: new import_reader_collection_page_repository.ReaderCollectionPageRepository({
	            responses: this.data.responses,
	            authScope: options.topic.authScope,
	            namespace: "bookmarks",
	            kind: "reader-bookmark-projection",
	            tags: ["bookmark-projection"],
	            normalizeRecord: import_reader_bookmark_model.normalizeStoredReaderBookmark,
	            sortRecords: import_reader_bookmark_model.sortReaderBookmarkRecords,
	            pageSize: 60,
	            retainForMs: 4320 * 60 * 6e4,
	            permanent: !0,
	            coordination: this.data.cacheCoordination
	          }),
	          native: this.bookmarkNative,
	          actions: this.bookmarkActions,
	          reactionEvents: this.#collectionActionEvents,
	          activityEvents: this.#collectionActionEvents,
	          cache: this.data.responses,
	          target: {
	            openTarget: async (request) => {
	              const result = await this.openTarget(request);
	              return (result.topic.status === "opened" || result.topic.status === "reused") && result.navigation?.status === "revealed";
	            }
	          },
	          ...bookmarkOptions.tabOrder === void 0 ? {} : { tabOrder: bookmarkOptions.tabOrder },
	          ...bookmarkOptions.pageSize === void 0 ? {} : { pageSize: bookmarkOptions.pageSize },
	          ...bookmarkOptions.liveRefreshDelayMs === void 0 ? {} : {
	            liveRefreshDelayMs: bookmarkOptions.liveRefreshDelayMs
	          },
	          backgroundWarmDelayMs: bookmarkOptions.backgroundWarmDelayMs ?? 2400,
	          visibleHistoryConcurrency: bookmarkOptions.visibleHistoryConcurrency ?? 3,
	          historyCoordination: this.data.cacheCoordination,
	          historyCoordinationKey: `reader-bookmark-history:v1:${options.topic.authScope}`,
	          ...bookmarkOptions.changeTabOrder === void 0 ? {} : {
	            changeTabOrder: bookmarkOptions.changeTabOrder
	          },
	          ...(bookmarkOptions.searchForms ?? options.searchForms) === void 0 ? {} : {
	            searchForms: bookmarkOptions.searchForms ?? options.searchForms
	          },
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "bookmark",
	            cause
	          )
	        }), this.bookmarkPanelView = new import_reader_bookmark_panel_view.ReaderBookmarkPanelView({
	          ...bookmarkOptions,
	          ...bookmarkOptions.renderIcon ? {} : options.renderIcon ? { renderIcon: options.renderIcon } : {},
	          document: options.document,
	          mount: this.shell.view.surfaceHost,
	          storage: options.storage,
	          controller: this.bookmarkController,
	          elements: readerBookmarkPanelElements(
	            this.shell.view.root
	          ),
	          baseUrl: topicBaseUrl,
	          relativeTime: nativeRelativeTime,
	          archiveMarker: (topicId, postNumber) => this.#historyArchiveMarker(topicId, postNumber),
	          reactionIconSource: bookmarkOptions.reactionIconSource ?? ((reaction) => (0, import_native_host_api.discourseNativeEmojiUrl)(
	            options.host,
	            reaction
	          )),
	          confirmDelete: bookmarkOptions.confirmDelete ?? ((request) => this.feedback.confirm({
	            title: request.title,
	            message: request.message,
	            note: "该操作会同步到 Discourse 收藏。",
	            confirmLabel: request.confirmLabel,
	            tone: "danger",
	            details: [{
	              label: "收藏数量",
	              value: String(request.count)
	            }]
	          })),
	          notify: (message) => this.feedback.show(message),
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "bookmark",
	            cause
	          )
	        });
	      }
	      this.notificationController?.startBackgroundCache(), this.bookmarkController?.startBackgroundCache();
	      const selfObservationUsername = (0, import_native_host_api.discourseNativeCurrentUsername)(options.host);
	      if (selfObservationUsername) {
	        const currentUser = options.host.lookup("service:current-user"), retrySelfPrivateSources = () => {
	          this.notificationController?.startBackgroundCache(), this.notificationController?.retryBackgroundCache(), this.bookmarkController?.startBackgroundCache(), this.bookmarkController?.retryBackgroundCache();
	        };
	        this.userObservations.observeSelf({
	          username: selfObservationUsername,
	          name: String(readerNativeModelValue(currentUser, "name") ?? "").trim(),
	          avatarTemplate: String(
	            readerNativeModelValue(currentUser, "avatar_template") ?? ""
	          ).trim()
	        }, retrySelfPrivateSources);
	        const publishSelfObservation = () => {
	          this.userObservations.updateSelfObservation(
	            (0, import_reader_self_observation_projection.readerSelfObservationProjection)({
	              ...this.notificationController ? {
	                notifications: {
	                  snapshot: this.notificationController.snapshot,
	                  records: this.notificationController.syncHistoryRecords()
	                }
	              } : {},
	              ...this.bookmarkController ? {
	                collections: {
	                  snapshot: this.bookmarkController.snapshot,
	                  records: this.bookmarkController.observationRecords()
	                }
	              } : {}
	            })
	          );
	        };
	        this.notificationController?.changes.subscribe(
	          publishSelfObservation,
	          this.scope
	        ), this.bookmarkController?.changes.subscribe(
	          publishSelfObservation,
	          this.scope
	        ), publishSelfObservation();
	      }
	      const coreTopicFactory = (0, import_reader_topic_factory.createReaderTopicFactory)({
	        ...topicFactoryOptions,
	        document: options.document,
	        onPhase: (phase, context) => {
	          options.loadingProgress?.update({
	            topicId: Number(context.topicId),
	            phase
	          });
	        },
	        createDomOptions: (bundle, context, root) => {
	          bundle.services.actions.events.subscribe(
	            (event) => this.#collectionActionEvents.emit(event),
	            context.scope
	          );
	          const currentUsername = (0, import_native_host_api.discourseNativeCurrentUsername)(options.host), topicImages = new import_reader_topic_image_index.ReaderTopicImageIndex({
	            document: options.document,
	            baseUrl: topicBaseUrl,
	            topicId: context.topicId,
	            session: bundle.services.session,
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "image-index",
	              cause
	            )
	          }), {
	            onError: mediaOnError,
	            visibility: mediaVisibility,
	            renderRetryIcon,
	            onLayoutChanged: mediaImageLayoutChanged,
	            onContentLayoutChanged: mediaContentLayoutChanged,
	            ...mediaOptions
	          } = options.media ?? {}, notifyTopicLayoutChanged = () => {
	            topicDoms.get(context.scope)?.notifyContentLayoutChanged();
	          }, topicMedia = new import_reader_topic_media_feature.ReaderTopicMediaFeature({
	            ...mediaOptions,
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            ...renderRetryIcon ? { renderRetryIcon } : options.renderIcon ? {
	              renderRetryIcon: (document) => options.renderIcon(
	                "rotate-ccw",
	                document
	              )
	            } : {},
	            document: options.document,
	            baseUrl: topicBaseUrl,
	            visibility: mediaVisibility ?? (() => options.document.visibilityState),
	            onLayoutChanged: (image) => {
	              mediaImageLayoutChanged?.(image), notifyTopicLayoutChanged();
	            },
	            onContentLayoutChanged: (root2) => {
	              mediaContentLayoutChanged?.(root2), notifyTopicLayoutChanged();
	            },
	            parentScope: context.scope,
	            onError: (cause) => {
	              try {
	                mediaOnError?.(cause);
	              } catch {
	              }
	              reportTopicFeature(
	                context.topicId,
	                "post-media",
	                cause
	              );
	            }
	          }), topicCookedContent = new import_reader_cooked_content_feature.ReaderCookedContentFeature({
	            document: options.document,
	            mount: this.shell.view.modal,
	            baseUrl: topicBaseUrl,
	            ...options.share ? { clipboard: options.share } : {},
	            ...this.blobDownloads ? { downloads: this.blobDownloads } : {},
	            notify: (message) => this.feedback.show(message),
	            onLayoutChanged: notifyTopicLayoutChanged,
	            onPrepared: (root2) => {
	              this.translationFeature?.syncPost(root2);
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "cooked-content",
	              cause
	            )
	          }), domOptions = createDomOptions(
	            bundle,
	            context,
	            root,
	            Object.freeze({
	              composer: this.composer,
	              presentation: nativeTopicPresentation,
	              relativeTime: nativeRelativeTime,
	              exactTime: nativeExactTime,
	              currentUsername,
	              recoverAvatarSource: (source) => this.#recoverAvatarSource(source)
	            })
	          ), replyTreePresentation = domOptions.replyTreePresentation ?? new import_reader_reply_tree_preferences.ReaderReplyTreePresentation(
	            bundle.replies.topology,
	            domOptions.replyTreePreferences?.read(),
	            {
	              canonicalCoverageComplete: () => bundle.replies.coverage().complete,
	              canonicalPostStreamRevision: () => bundle.session.postStreamRevision ?? 0,
	              canonicalPostStreamGapCount: (postNumber, previousRootPostNumber) => bundle.session.postStreamGapCount?.(
	                previousRootPostNumber,
	                postNumber
	              )
	            }
	          ), topicPresentationChanges = new import_signal.Signal(), topicPostCommands = new import_post_action_feature_commands.PostActionFeatureCommands(
	            bundle.services.postActions
	          ), topicPoll = new import_reader_poll_feature.ReaderTopicPollFeature({
	            document: options.document,
	            actions: bundle.services.actions,
	            commands: topicPostCommands,
	            descriptors: actionDescriptors,
	            readPost: (postId) => bundle.services.session.postById(postId),
	            viewer: () => readerPollViewer(
	              nativePostModels.currentUser(),
	              currentUsername
	            ),
	            topicArchived: () => bundle.services.session.topic?.archived === !0,
	            notify: (message) => this.feedback.show(message),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "poll",
	              cause
	            )
	          }), topicBookmarkActions = new import_reader_bookmark_action_coordinator.ReaderBookmarkActionCoordinator({
	            topicId: context.topicId,
	            session: bundle.services.session,
	            actions: bundle.services.actions,
	            postCommands: topicPostCommands,
	            descriptors: actionDescriptors,
	            forms: nativeBookmarkForm,
	            models: nativePostModels
	          }), topicShareActions = options.share ? new import_reader_share_action_coordinator.ReaderShareActionCoordinator({
	            topicId: context.topicId,
	            topic: () => {
	              const topic = bundle.services.session.topic;
	              if (!topic)
	                throw new Error(
	                  "分享链接时 canonical Topic 尚未就绪"
	                );
	              return topic;
	            },
	            links: nativeTopicLinks,
	            surface: options.share,
	            fallbackTitle: () => options.document.title || "LINUX DO"
	          }) : null, topicNotificationActions = new import_reader_topic_notification_coordinator.ReaderTopicNotificationCoordinator({
	            topicId: context.topicId,
	            session: bundle.services.session,
	            actions: bundle.services.actions,
	            descriptors: actionDescriptors,
	            models: nativePostModels
	          }), topicSharedIssueActions = new import_reader_topic_shared_issue_coordinator.ReaderTopicSharedIssueCoordinator({
	            topicId: context.topicId,
	            session: bundle.services.session,
	            actions: bundle.services.actions,
	            descriptors: actionDescriptors,
	            settings: nativePostModels,
	            currentUsername
	          }), assignmentAbort = context.scope.abortController(
	            new Error("Reader Topic 指定表单生命周期已结束")
	          ), topicManagementActions = new import_reader_post_management_action_coordinator.ReaderPostManagementActionCoordinator({
	            topicId: context.topicId,
	            session: bundle.services.session,
	            actions: bundle.services.actions,
	            postCommands: topicPostCommands,
	            descriptors: actionDescriptors,
	            models: nativePostModels,
	            composer: this.composer,
	            assignments: this.assignmentForm,
	            assignmentSignal: assignmentAbort.signal,
	            feedback: this.feedback,
	            adminMenu: nativeAdminMenu,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "post-action",
	              cause
	            )
	          }), topicPostActions = new import_reader_post_action_feature.ReaderPostActionFeature({
	            document: options.document,
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            surfaceHost: this.shell.view.surfaceHost,
	            topic: () => {
	              const topic = bundle.services.session.topic;
	              if (!topic)
	                throw new Error(
	                  "楼层动作渲染时 canonical Topic 尚未就绪"
	                );
	              return topic;
	            },
	            actions: bundle.services.actions,
	            commands: topicPostCommands,
	            descriptors: actionDescriptors,
	            models: nativePostModels,
	            reactions: postReactions,
	            capabilityInput: (post) => {
	              const topic = bundle.services.session.topic, currentUser = nativePostModels.currentUser();
	              return Object.freeze({
	                post,
	                ...topic ? { topic } : {},
	                ...currentUser ? { currentUser } : {},
	                currentUsername: (0, import_native_host_api.discourseNativeCurrentUsername)(
	                  options.host
	                ),
	                plugins: Object.freeze({
	                  boosts: (0, import_native_host_api.discourseNativeBoostsAvailable)(
	                    options.host
	                  ) || Object.hasOwn(post, "can_boost") || Array.isArray(post.boosts)
	                })
	              });
	            },
	            topicActionRail: !!options.topicActionRail,
	            refreshMissingCapabilities: async (post) => {
	              const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(
	                post.post_number
	              );
	              postNumber !== null && await bundle.services.session.loadTarget(postNumber, {
	                scope: "around",
	                forceRefresh: !0,
	                advanceCursor: !1
	              });
	            },
	            presentation: nativeTopicPresentation,
	            currentUsername,
	            ...options.boostCopy ? {
	              readBoostCopySettings: options.boostCopy.readSettings
	            } : {},
	            emojiMenu: nativeEmojiMenu,
	            confirmBoostDelete: ({ username }) => this.feedback.confirm({
	              title: "删除 Boost",
	              message: username ? `确认删除 @${username} 的这条 Boost?` : "确认删除这条 Boost?",
	              note: "该操作会同步到 Discourse。",
	              confirmLabel: "删除",
	              tone: "danger"
	            }),
	            reportBoost: async ({
	              postId,
	              boostId,
	              username
	            }) => {
	              const access = await bundle.services.boostReportAccess.load(
	                boostId
	              );
	              if (!access.canFlag)
	                throw new Error(
	                  access.alreadyFlagged ? "你已经举报过这个 Boost" : "当前账号不能举报这个 Boost"
	                );
	              const available = new Set(
	                access.availableFlagNames
	              ), reportOptions = readerReportOptions(
	                options.document,
	                nativeFlagCatalog.flagTypes(),
	                available,
	                "DiscourseBoosts::Boost"
	              ), reportedUsername = access.username || username;
	              return this.reportForm.open({
	                title: "举报 Boost",
	                intro: reportedUsername ? `举报 @${reportedUsername} 的 Boost 会直接提交给社区,不会离开阅读器。` : "举报会直接提交给社区,不会离开阅读器。",
	                options: reportOptions,
	                messageMaxLength: nativeFlagCatalog.messageMaxLength(),
	                submit: async ({
	                  optionId,
	                  message
	                }) => (await bundle.services.actions.dispatch(
	                  topicPostCommands.boostReport(
	                    postId,
	                    actionDescriptors.boostReport({
	                      boostId,
	                      flagTypeId: optionId,
	                      ...message ? { message } : {}
	                    })
	                  )
	                ), "举报已提交")
	              });
	            },
	            reportPost: async (post) => {
	              const topic = bundle.services.session.topic;
	              if (!topic)
	                throw new Error(
	                  "楼层举报时 canonical Topic 尚未就绪"
	                );
	              const flagTypes = nativeFlagCatalog.flagTypes(), native = nativePostModels.reportContext(
	                topic,
	                post,
	                flagTypes.map((flag) => flag.nameKey)
	              ), actionByName = new Map(
	                native.actions.map((entry) => [
	                  entry.nameKey,
	                  entry.action
	                ])
	              ), reportOptions = readerReportOptions(
	                options.document,
	                flagTypes,
	                new Set(actionByName.keys()),
	                "Post"
	              ), flagById = new Map(
	                flagTypes.map((flag) => [
	                  flag.id,
	                  flag
	                ])
	              ), postId = Number(post.id), postNumber = Number(post.post_number), topicStarter = postNumber === 1;
	              return this.reportForm.open({
	                title: topicStarter ? "举报主题" : "举报楼层",
	                intro: topicStarter ? "举报主题会直接提交给社区,不会离开阅读器。" : postNumber > 0 ? `举报 #${postNumber} 楼会直接提交给社区,不会离开阅读器。` : "举报会直接提交给社区,不会离开阅读器。",
	                options: reportOptions,
	                messageMaxLength: nativeFlagCatalog.messageMaxLength(),
	                submit: async ({
	                  optionId,
	                  message
	                }) => {
	                  const flag = flagById.get(optionId), postAction = flag ? actionByName.get(flag.nameKey) : null;
	                  if (!flag || !postAction)
	                    throw new Error(
	                      "当前举报类型已不可用,请重新打开表单"
	                    );
	                  return await bundle.services.actions.dispatch(
	                    topicPostCommands.report(
	                      postId,
	                      actionDescriptors.postReport({
	                        postId,
	                        post: native.post,
	                        postAction,
	                        flagTypeId: optionId,
	                        ...message ? { message } : {}
	                      })
	                    )
	                  ), "举报已提交";
	                }
	              });
	            },
	            bookmarks: topicBookmarkActions,
	            ...topicShareActions ? { shares: topicShareActions } : {},
	            topicNotifications: topicNotificationActions,
	            sharedIssue: topicSharedIssueActions,
	            management: topicManagementActions,
	            notify: (message) => this.feedback.show(message),
	            composer: this.composer,
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "post-action",
	              cause
	            )
	          });
	          let topicActionRail = null;
	          const topicSummaryImagePicker = this.imageResources ? new import_reader_lightbox_image_picker.ReaderLightboxImagePicker({
	            document: options.document,
	            mount: this.shell.view.surfaceHost,
	            catalog: topicImages,
	            originalSources: this.imageResources,
	            maximumSelected: 6,
	            notify: (message) => this.feedback.show(message),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "image-index",
	              cause
	            )
	          }) : null, topicSummaryPreview = options.lightbox && options.resources ? { open: null } : null;
	          topicSummaryPreview && (topicSummaryPreviews.set(context.scope, topicSummaryPreview), context.scope.add(() => topicSummaryPreviews.delete(context.scope)));
	          const topicSummarySurface = topicSummaryTransport && options.topicActionRail ? new import_reader_topic_summary_surface.ReaderTopicSummarySurface({
	            document: options.document,
	            mount: this.shell.view.surfaceHost,
	            request: new import_reader_topic_summary_request_adapter.ReaderTopicSummaryRequestAdapter({
	              gateway: this.data.gateway,
	              transport: topicSummaryTransport,
	              authScope: options.topic.authScope,
	              topicId: context.topicId,
	              signal: context.signal,
	              ...options.topic.basePath === void 0 ? {} : { basePath: options.topic.basePath }
	            }),
	            ...this.translationRequests ? {
	              aiModels: this.translationRequests,
	              customRequest: new import_reader_topic_custom_summary.ReaderTopicCustomSummaryRequestAdapter({
	                document: options.document,
	                baseUrl: topicBaseUrl,
	                session: bundle.services.session,
	                topology: bundle.services.replies.topology,
	                completion: this.translationRequests,
	                signal: context.signal
	              })
	            } : {},
	            ...topicSummaryImagePicker && this.imageResources ? {
	              imagePicker: topicSummaryImagePicker,
	              imageResources: this.imageResources
	            } : {},
	            uploader: new import_reader_topic_summary_request_adapter.ReaderTopicSummaryImageUploadAdapter({
	              gateway: this.data.gateway,
	              transport: topicSummaryTransport,
	              authScope: options.topic.authScope,
	              topicId: context.topicId,
	              signal: context.signal,
	              createFormData: () => {
	                const Constructor = options.document.defaultView?.FormData ?? FormData;
	                return new Constructor();
	              },
	              ...options.topic.basePath === void 0 ? {} : { basePath: options.topic.basePath }
	            }),
	            topicTitle: () => {
	              const topic = bundle.services.session.topic;
	              return String(
	                topic?.title ?? options.document.title ?? ""
	              );
	            },
	            topicUrl: () => nativeTopicLinks.topicHref(context.topicId),
	            openReply: async (raw) => {
	              const topic = bundle.services.session.topic, firstPost = bundle.services.session.postByNumber(1);
	              if (!topic || !firstPost)
	                throw new Error("当前主题 #1 楼尚未就绪");
	              await this.composer.openReply({
	                topic,
	                post: firstPost,
	                initialRaw: raw
	              });
	            },
	            ...options.share ? { clipboard: options.share } : {},
	            ...this.blobDownloads ? { downloads: this.blobDownloads } : {},
	            settingsStorage: options.storage,
	            positionMode: () => this.shell.view.root.dataset.readerWorkspaceMode ?? "floating",
	            ...options.topicSummaryFonts ? { fonts: options.topicSummaryFonts } : {},
	            ...topicSummaryPreview ? {
	              previewImage: (input) => {
	                if (!topicSummaryPreview.open)
	                  throw new Error("主题灯箱尚未完成装配");
	                topicSummaryPreview.open(input);
	              }
	            } : {},
	            notify: (message) => this.feedback.show(message),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "post-action",
	              cause
	            )
	          }) : null;
	          topicActionRail = options.topicActionRail ? new import_reader_topic_action_rail.ReaderTopicActionRail({
	            document: options.document,
	            mount: this.shell.view.modal,
	            shellRoot: this.shell.view.root,
	            identity: domOptions.identity,
	            actions: topicPostActions,
	            preferences: options.topicActionRail,
	            jumpToTop: async () => {
	              const timeline = topicTimelines.get(context.scope);
	              if (!timeline)
	                throw new Error(
	                  "主帖操作列跳转时 Topic timeline 尚未就绪"
	                );
	              const result = await timeline.jumpTo(1, {
	                alignment: "start",
	                highlight: !0
	              });
	              if (result.status !== "revealed")
	                throw new Error(
	                  `主帖操作列回顶失败:${result.status}`
	                );
	            },
	            ...topicSummarySurface ? { openTopicSummary: () => topicSummarySurface.open() } : {},
	            ...options.downloadCurrentTopic ? { downloadCurrentTopic: options.downloadCurrentTopic } : {},
	            openChronicle: () => this.chronicleView.open(),
	            openUnwantedTopics: () => this.unwantedTopicView.open(),
	            openUserObservations: () => this.userObservationView.openList(),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "post-action",
	              cause
	            )
	          }) : null, topicActionRail && (0, import_reader_topic_action_rail.bindReaderTopicActionRailStarter)({
	            readStarter: () => bundle.services.session.postByNumber(1),
	            loadStarter: () => bundle.services.session.loadTarget(1, {
	              scope: "single",
	              advanceCursor: !1
	            }),
	            waitUntilReady: () => bundle.services.session.init(),
	            subscribe: (listener, scope) => bundle.services.session.changes.subscribe(
	              listener,
	              scope
	            ),
	            update: (starter) => topicActionRail.update(starter),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "post-action",
	              cause
	            )
	          });
	          const topicSpecialContent = new import_reader_topic_special_content_feature.ReaderTopicSpecialContentFeature({
	            document: options.document,
	            session: bundle.services.session,
	            presentationChanges: topicPresentationChanges,
	            presentation: nativeTopicPresentation,
	            relativeTime: nativeRelativeTime,
	            actions: bundle.services.actions,
	            commands: topicPostCommands,
	            descriptors: actionDescriptors,
	            models: nativePostModels,
	            loadPostVotingComments: (postId, afterCommentId) => bundle.services.requests.loadPostVotingComments(postId, {
	              afterCommentId,
	              refresh: !0
	            }),
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            navigate: async (postNumber) => {
	              const navigation = topicNavigations.get(
	                context.scope
	              );
	              if (!navigation)
	                throw new Error(
	                  "特殊正文跳转时 Topic navigation 尚未就绪"
	                );
	              const result = await navigation.navigate({
	                postNumber,
	                source: "solved-answer",
	                alignment: "center",
	                highlight: !0
	              });
	              if (result.status !== "revealed")
	                throw new Error(
	                  `特殊正文楼层 #${postNumber} 跳转失败:${result.status}`
	                );
	            },
	            onBodyLayerChanged: (view) => {
	              topicCookedContent.refresh(view), topicMedia.refresh(view), this.translationFeature?.syncPost(
	                view.slots.root
	              ), notifyTopicLayoutChanged();
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "special-content",
	              cause
	            )
	          }), topicContext = new import_reader_topic_context_controller.ReaderTopicContextController({
	            session: bundle.services.session,
	            replies: bundle.replies,
	            loadCrossTopicQuotedPost: async (targetTopicId, targetPostNumber) => {
	              const targetOptions = { scope: "single" };
	              let lastError;
	              for (const candidate of bundle.services.requests.targetCandidates(
	                targetPostNumber,
	                targetOptions,
	                targetTopicId
	              ))
	                try {
	                  const payload = await bundle.services.requests.loadTargetCandidate(
	                    candidate,
	                    targetPostNumber,
	                    targetOptions,
	                    targetTopicId
	                  ), post = (0, import_topic_session.discoursePostsFromPayload)(payload).find((value) => Number(value.post_number) === targetPostNumber);
	                  if (post) return post;
	                } catch (error) {
	                  lastError = error;
	                  const status = Number(
	                    error?.status
	                  );
	                  if (error instanceof DOMException && error.name === "AbortError") throw error;
	                  if ((0, import_native_request_descriptors.discourseNativeTargetFailureIsDefinitive)({
	                    endpoint: candidate.endpoint,
	                    scope: targetOptions.scope,
	                    status
	                  })) return null;
	                  if ([401, 403, 429].includes(status)) throw error;
	                }
	              if (lastError) throw lastError;
	              return null;
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "thread-context",
	              cause
	            )
	          }), topicScroll = new import_reader_topic_scroll_adapter.ReaderTopicScrollAdapter({
	            ...options.navigation,
	            scrollRoot: this.shell.view.body,
	            viewportChangeTarget: this.shell.view.root,
	            parentScope: context.scope
	          }), topicContextFeature = new import_reader_topic_context_surface.ReaderTopicContextFeature({
	            document: options.document,
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            controller: topicContext,
	            replies: bundle.replies,
	            presentationChanges: topicPresentationChanges,
	            presentation: replyTreePresentation,
	            avatarSource: (template, size) => nativeTopicPresentation.avatarSource(template, size),
	            scrollRoot: this.shell.view.body,
	            quoteHintHost: this.shell.view.surfaceHost,
	            notify: (message) => this.feedback.show(message),
	            navigate: () => topicNavigations.get(context.scope) ?? null,
	            target: {
	              open: async (request) => {
	                await this.openTarget(request);
	              }
	            },
	            onQuoteBodyChanged: (view) => {
	              topicMedia.refresh(view), this.translationFeature?.syncPost(
	                view.slots.root
	              );
	            },
	            onRevealNextReplyLevel: (postNumber) => topicDoms.get(context.scope)?.revealNextReplyLevel(postNumber) ?? !1,
	            revealQuoteTarget: (target, mode) => {
	              typeof target.getBoundingClientRect == "function" && topicScroll.alignPost(target, {
	                source: mode === "match" ? "quote-match" : "quote",
	                alignment: mode === "match" ? "nearest" : "start",
	                highlight: mode === "floor"
	              });
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "thread-context",
	              cause
	            )
	          }), topicCommentsHeader = new import_reader_topic_comments_header.ReaderTopicCommentsHeader({
	            document: options.document,
	            topicId: context.topicId,
	            session: bundle.services.session,
	            presence: nativePresence,
	            presentation: nativeTopicPresentation,
	            currentUsername,
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "topic-header",
	              cause
	            )
	          }), topicLocalArchive = new import_reader_topic_local_archive_feature.ReaderTopicLocalArchiveFeature({
	            document: options.document,
	            topicRoot: root,
	            session: bundle.services.session,
	            parentScope: context.scope
	          }), postAuthorFilter = options.unwantedTopicFilter ? new import_reader_post_author_filter_feature.ReaderPostAuthorFilterFeature({
	            preferences: options.unwantedTopicFilter,
	            parentScope: context.scope
	          }) : null;
	          if (context.scope.add(
	            bundle.services.snapshots.setPersistenceDelayReader(
	              (minimumIdleMs) => topicScroll.remainingUserIdleMs(minimumIdleMs)
	            )
	          ), domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_topic_media_feature.ReaderTopicMediaFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderTopicMediaFeature"
	            );
	          if (domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_cooked_content_feature.ReaderCookedContentFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderCookedContentFeature"
	            );
	          if (domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_topic_special_content_feature.ReaderTopicSpecialContentFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderTopicSpecialContentFeature"
	            );
	          if (domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_topic_context_surface.ReaderTopicContextFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderTopicContextFeature"
	            );
	          if (domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_post_action_feature.ReaderPostActionFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderPostActionFeature"
	            );
	          if (domOptions.postFeatures?.some(
	            (feature) => feature instanceof import_reader_poll_feature.ReaderTopicPollFeature
	          ))
	            throw new Error(
	              "ReaderBrowserRuntime 已拥有唯一 ReaderTopicPollFeature"
	            );
	          const postFeatures = Object.freeze([
	            topicLocalArchive,
	            ...postAuthorFilter ? [postAuthorFilter] : [],
	            topicCookedContent,
	            topicPoll,
	            topicSpecialContent,
	            topicContextFeature,
	            topicPostActions,
	            topicCommentsHeader,
	            ...domOptions.postFeatures ?? [],
	            topicMedia
	          ]);
	          return topicFeatures.set(context.scope, Object.freeze({
	            topicImages,
	            topicMedia,
	            topicCookedContent,
	            topicSpecialContent,
	            topicContext,
	            topicContextFeature,
	            highlightDiscussionTarget: (target) => topicScroll.highlight.highlight(target),
	            topicActionRail,
	            topicPostActions,
	            presentation: Object.freeze({
	              identity: domOptions.identity,
	              renderPost: domOptions.render,
	              postFeatures
	            })
	          })), Object.freeze({
	            ...domOptions,
	            replyTreePresentation,
	            presentationChanges: topicPresentationChanges,
	            postFeatures,
	            scroll: topicScroll,
	            readDirectReplyPrefetchScreens: () => this.#performance.nestedPrefetchScreens,
	            readDirectReplyPrefetchIdleMs: () => Math.max(
	              120,
	              this.#performance.requestMinIntervalMs + 30
	            ),
	            readDirectReplyPrefetchConcurrency: () => Math.max(
	              1,
	              Math.min(
	                2,
	                this.#performance.requestMaxConcurrent - 1
	              )
	            ),
	            ...options.topicFlowScheduler === void 0 ? {} : {
	              directReplyPrefetchScheduler: {
	                schedule: (callback, delayMs) => options.topicFlowScheduler.schedule(
	                  callback,
	                  "near-window",
	                  delayMs
	                ),
	                cancel: (handle) => options.topicFlowScheduler.cancel(
	                  handle
	                )
	              }
	            }
	          });
	        },
	        createBundle: (context) => this.data.createTopicBundle(
	          context,
	          {
	            ...options.topic,
	            pageSize: this.#performance.pageSize,
	            host: options.host,
	            nativeAjax: this.nativeAjax,
	            nativeActions,
	            composerEvents: this.composerIsolation,
	            ...options.loadingProgress ? {
	              onLoadingSource: (source, counts) => {
	                options.loadingProgress.update({
	                  topicId: Number(context.topicId),
	                  phase: source,
	                  cachedCount: counts.cachedCount,
	                  missingCount: counts.missingCount
	                });
	              }
	            } : {}
	          }
	        ),
	        onAssembled: (value, context) => {
	          topicDoms.set(context.scope, value.dom);
	          const features = topicFeatures.get(context.scope);
	          if (!features)
	            throw new Error(
	              "Topic context surface 装配时 presentation 尚未就绪"
	            );
	          const translationGeneration = this.translationFeature?.activateTopic(context.topicId);
	          context.scope.add(() => this.translationFeature?.deactivateTopic(
	            context.topicId,
	            translationGeneration
	          ));
	          let translationWindowPostNumbers = /* @__PURE__ */ new Set();
	          const updateTranslationWindow = () => {
	            const posts = [...translationWindowPostNumbers].map((postNumber) => value.services.session.postByNumber(postNumber)).filter((post) => post !== void 0);
	            this.translationFeature?.updatePreloadWindow(
	              context.topicId,
	              posts,
	              translationGeneration
	            );
	          };
	          value.dom.windowChanges.subscribe((commit) => {
	            translationWindowPostNumbers = /* @__PURE__ */ new Set([
	              ...commit.tree.mountedRoots,
	              ...commit.tree.mountedReplies
	            ]), updateTranslationWindow(), this.translationFeature?.syncMountedPosts();
	          }, context.scope), value.services.session.changes.subscribe((commit) => {
	            let changedWindow = !1;
	            for (const postNumber of commit.changedPostNumbers) {
	              const post = value.services.session.postByNumber(postNumber), parentPostNumber = (0, import_identifiers.tryDiscoursePostNumber)(
	                post?.reply_to_post_number
	              );
	              !translationWindowPostNumbers.has(postNumber) && (parentPostNumber === null || !translationWindowPostNumbers.has(parentPostNumber)) || (translationWindowPostNumbers.add(postNumber), changedWindow = !0);
	            }
	            changedWindow && updateTranslationWindow();
	          }, context.scope), value.services.session.archiveChanges.subscribe(() => {
	            const active = this.shell.activeValue;
	            active?.services.session === value.services.session && (this.#rememberHistoryTopicMetadata(active), this.#rememberChronicleArchives(active), this.#collectChronicleRequests(this.data.requests.snapshot));
	          }, context.scope), this.#rememberChronicleArchives(value);
	          let contextSurface = null;
	          const navigation = new import_reader_topic_navigation_controller.ReaderTopicNavigationController({
	            session: value.services.session,
	            dom: value.dom,
	            hidden: {
	              isHidden: (postNumber) => value.dom.isPostHidden(postNumber),
	              async revealPost(postNumber) {
	                const surface = contextSurface;
	                return surface ? surface.revealDiscussionPost(postNumber) : null;
	              }
	            },
	            listenUserScrollIntent: (listener) => value.dom.listenUserScrollIntent(listener),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "navigation",
	              cause
	            )
	          });
	          topicNavigations.set(context.scope, navigation);
	          const topicFlow = new import_reader_topic_flow_controller.ReaderTopicFlowController({
	            dom: value.dom,
	            readPerformance: () => this.#performance,
	            sessionChanges: value.services.session.changes,
	            readLoadDone: () => value.services.session.loadDone,
	            ...options.topicFlowScheduler === void 0 ? {} : {
	              scheduler: options.topicFlowScheduler
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "navigation",
	              cause
	            )
	          });
	          topicFlows.set(context.scope, topicFlow), contextSurface = new import_reader_topic_context_surface.ReaderTopicContextSurface({
	            document: options.document,
	            controller: features.topicContext,
	            replies: value.replies,
	            discussionHost: this.shell.view.modal,
	            workspace: this.workspace.workspace,
	            identity: features.presentation.identity,
	            renderPost: features.presentation.renderPost,
	            postFeatures: features.presentation.postFeatures,
	            postProjector: value.dom.postProjector,
	            readDiscussionMaterializedPostLimit: () => Math.max(
	              12,
	              Math.floor(
	                this.#performance.streamMaxMountedPostCount / 2
	              )
	            ),
	            highlight: features.highlightDiscussionTarget,
	            stateRepository: this.threadContextState,
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "thread-context",
	              cause
	            )
	          }), topicContextSurfaces.set(context.scope, contextSurface), features.topicContextFeature.connectQuoteSource({
	            captureAnchor: () => {
	              const viewport = value.dom.captureViewportAnchor();
	              return viewport ? Object.freeze({
	                viewport: Object.freeze({
	                  ...viewport,
	                  postNumber: (0, import_identifiers.discoursePostNumber)(
	                    viewport.postNumber
	                  )
	                }),
	                replyWindow: contextSurface.captureDiscussionState(),
	                quoteHighlight: null
	              }) : null;
	            },
	            restore: async (source) => this.#restoreQuoteSource(source)
	          }), this.translationFeature?.syncMountedPosts();
	          const header = new import_reader_topic_header.ReaderTopicHeaderController({
	            session: value.services.session,
	            presentation: nativeTopicPresentation,
	            parentScope: context.scope,
	            onError: (cause) => {
	              reportTopicFeature(
	                context.topicId,
	                "topic-header",
	                cause
	              ), this.feedback.show(
	                cause instanceof Error ? cause.message : "打开帖子编辑器失败"
	              );
	            }
	          });
	          topicHeaders.set(context.scope, header);
	          const onlyOp = new import_reader_topic_only_op_controller.ReaderTopicOnlyOpController({
	            session: value.services.session,
	            presentationChanges: value.dom.presentationChanges,
	            presentation: value.dom.replyTreePresentation,
	            onProjectionChanged: (resetScroll) => {
	              value.dom.refreshRootProjection(resetScroll);
	            },
	            onEnabledChanged: (enabled) => {
	              topicFlow.setProjectionPriority(enabled);
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "navigation",
	              cause
	            )
	          });
	          topicOnlyOpControllers.set(context.scope, onlyOp);
	          const topicVoteSession = value.services.session, topicVoteCommands = new import_topic_action_feature_commands.TopicActionFeatureCommands({
	            topicId: context.topicId,
	            session: topicVoteSession
	          }), headerView = new import_reader_topic_header.ReaderTopicHeaderView({
	            controller: header,
	            hostDocument: options.document,
	            elements: readerTopicHeaderElements(
	              this.shell.view.root
	            ),
	            onlyOp,
	            ...options.renderIcon ? { renderIcon: options.renderIcon } : {},
	            onJumpFirst: async () => {
	              const result = await navigation.navigate({
	                postNumber: 1,
	                source: "link",
	                alignment: "start",
	                highlight: !0
	              });
	              if (result.status !== "revealed")
	                throw new Error(
	                  `主题标题跳转失败:${result.status}`
	                );
	            },
	            onToggleTopicVote: async (voted) => {
	              if (!(0, import_native_host_api.discourseNativeCurrentUsername)(options.host))
	                throw new Error("登录后才能为主题投票。");
	              await value.services.actions.dispatch(topicVoteCommands.vote(
	                voted,
	                actionDescriptors.topicVoteToggle({
	                  topicId: context.topicId,
	                  voted
	                })
	              ));
	            },
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "topic-header",
	              cause
	            )
	          });
	          topicHeaderViews.set(context.scope, headerView);
	          const topicEditTrigger = this.shell.view.root.querySelector(
	            ".ldp-topic-edit-trigger"
	          );
	          if (!topicEditTrigger)
	            throw new Error("Reader Header 缺少 Topic 编辑入口");
	          new import_reader_topic_edit_controller.ReaderTopicEditController({
	            topicId: context.topicId,
	            session: value.services.session,
	            trigger: topicEditTrigger,
	            form: this.topicEditForm,
	            catalog: nativeTopicEditCatalog,
	            actions: value.services.actions,
	            descriptors: actionDescriptors,
	            models: nativePostModels,
	            notify: (message) => this.feedback.show(message),
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "topic-header",
	              cause
	            )
	          });
	          const readTotalPostCount = () => {
	            const topic = value.services.session.topic ?? value.topic, candidates = [
	              Number(topic.highest_post_number),
	              Number(topic.posts_count),
	              ...value.services.session.cachedPosts().map((post) => (0, import_identifiers.tryDiscoursePostNumber)(post.post_number) ?? 0)
	            ];
	            return Math.max(
	              1,
	              ...candidates.filter((candidate) => Number.isSafeInteger(candidate) && candidate > 0)
	            );
	          }, timelinePresentation = value.dom.replyTreePresentation, timeline = new import_reader_topic_timeline_controller.ReaderTopicTimelineController({
	            navigation,
	            readTotalPostCount,
	            readNavigablePostNumbers: () => timelinePresentation.roots(),
	            readNavigablePostNumbersComplete: () => !timelinePresentation.canonicalFrozen && timelinePresentation.coverageComplete,
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "navigation",
	              cause
	            )
	          });
	          topicTimelines.set(context.scope, timeline);
	          const timelineViewOptions = options.timelineView;
	          if (timelineViewOptions) {
	            const {
	              readPreferences,
	              preferences,
	              formatRelative,
	              notify,
	              ...viewOptions
	            } = timelineViewOptions, timestamp = (value2) => typeof value2 == "string" && value2.trim() ? value2 : null, currentTopic = () => value.services.session.topic ?? value.topic, timelineView = new import_reader_topic_timeline_view.ReaderTopicTimelineView({
	              ...viewOptions,
	              controller: timeline,
	              elements: readerTopicTimelineElements(
	                this.shell.view.root
	              ),
	              preferences: readPreferences?.() ?? preferences,
	              readCreatedAt: (postNumber) => {
	                const post = value.services.session.postByNumber(postNumber);
	                return timestamp(post?.created_at) ?? (postNumber === 1 ? timestamp(currentTopic().created_at) : null);
	              },
	              readLatestReplyAt: () => timestamp(currentTopic().last_posted_at),
	              formatRelative: formatRelative ?? nativeRelativeTime,
	              notify: notify ?? ((message) => this.feedback.show(message)),
	              parentScope: context.scope,
	              onError: (cause) => reportTopicFeature(
	                context.topicId,
	                "navigation",
	                cause
	              )
	            });
	            topicTimelineViews.set(context.scope, timelineView);
	          }
	          const liveNavigation = new import_reader_topic_live_navigation_controller.ReaderTopicLiveNavigationController({
	            live: value.services.live,
	            navigation,
	            parentScope: context.scope,
	            onError: (cause) => reportTopicFeature(
	              context.topicId,
	              "navigation",
	              cause
	            )
	          });
	          topicLiveNavigations.set(context.scope, liveNavigation), topicLiveNavigationViews.set(
	            context.scope,
	            new import_reader_topic_live_navigation_view.ReaderTopicLiveNavigationView({
	              navigation: liveNavigation,
	              elements: readerTopicLiveNavigationElements(
	                this.shell.view.root
	              ),
	              notify: (message) => this.feedback.show(message),
	              parentScope: context.scope
	            })
	          ), value.dom.visibleRootChanges.subscribe((change) => {
	            const visibleRootPostNumber = timelinePresentation.rootOf(change.postNumber) ?? change.postNumber;
	            timeline.syncVisiblePost(visibleRootPostNumber, change), liveNavigation.syncViewport(change);
	          }, context.scope), value.services.session.changes.subscribe(() => {
	            timeline.refresh(), topicTimelineViews.get(context.scope)?.refresh();
	          }, context.scope), value.services.composerEvents.changes.subscribe((commit) => {
	            commit.postNumber !== null && navigation.navigate({
	              postNumber: commit.postNumber,
	              source: "composer",
	              alignment: "nearest",
	              focus: !0,
	              highlight: !0
	            }).catch(() => {
	            });
	          }, context.scope);
	        }
	      });
	      this.topicFactory = async (context) => {
	        const result = await coreTopicFactory(context), openNative2 = this.shell.view.root.querySelector(
	          ".ldp-open"
	        );
	        if (!openNative2)
	          throw new Error("Reader Shell 缺少原生主题入口");
	        const nativeHref = (0, import_reader_native_topic_route.readerNativeTopicHref)(
	          nativeTopicLinks.topicHref(context.topicId),
	          topicBaseUrl
	        );
	        openNative2.href = nativeHref, openNative2.hidden = !nativeHref, context.scope.add(() => {
	          openNative2.href === nativeHref && (openNative2.removeAttribute("href"), openNative2.hidden = !0);
	        });
	        const features = topicFeatures.get(context.scope);
	        if (!features) throw new Error("Topic presentation features 未完成装配");
	        const topicHeader = topicHeaders.get(context.scope);
	        if (!topicHeader) throw new Error("Topic header 未完成装配");
	        const topicHeaderView = topicHeaderViews.get(context.scope);
	        if (!topicHeaderView) throw new Error("Topic header View 未完成装配");
	        const topicOnlyOp = topicOnlyOpControllers.get(context.scope);
	        if (!topicOnlyOp) throw new Error("只看楼主控制器未完成装配");
	        const topicNavigation = topicNavigations.get(context.scope);
	        if (!topicNavigation) throw new Error("Topic navigation 未完成装配");
	        const topicFlow = topicFlows.get(context.scope);
	        if (!topicFlow) throw new Error("Topic flow 未完成装配");
	        const topicContextSurface = topicContextSurfaces.get(context.scope);
	        if (!topicContextSurface)
	          throw new Error("Topic context surface 未完成装配");
	        const topicTimeline = topicTimelines.get(context.scope);
	        if (!topicTimeline) throw new Error("Topic timeline 未完成装配");
	        const topicTimelineView = topicTimelineViews.get(context.scope) ?? null, topicLiveNavigation = topicLiveNavigations.get(context.scope);
	        if (!topicLiveNavigation)
	          throw new Error("Topic live navigation 未完成装配");
	        const topicLiveNavigationView = topicLiveNavigationViews.get(context.scope);
	        if (!topicLiveNavigationView)
	          throw new Error("Topic live navigation View 未完成装配");
	        let value = null;
	        const lightboxOptions = options.lightbox, lightboxPostCommands = new import_post_action_feature_commands.PostActionFeatureCommands(
	          result.value.services.postActions
	        ), topicLightbox = lightboxOptions ? new import_reader_lightbox_feature.ReaderLightboxFeature({
	          document: options.document,
	          mount: typeof lightboxOptions.mount == "function" ? lightboxOptions.mount() : lightboxOptions.mount,
	          topic: () => result.value.services.session.topic ?? result.value.topic,
	          session: result.value.services.session,
	          replies: result.value.replies,
	          composer: this.composer,
	          identity: features.presentation.identity,
	          renderPost: features.presentation.renderPost,
	          postFeatures: features.presentation.postFeatures,
	          reactionSurfaces: features.topicPostActions,
	          postProjector: result.value.dom.postProjector,
	          minimumCommentLength: () => nativePostModels.minimumPostLength(),
	          submitComment: async ({ topic, targetPost, raw }) => (await this.composer.openReply({
	            topic,
	            post: targetPost,
	            initialRaw: raw,
	            replaceRaw: !0
	          }), result.value.services.actions.dispatch(
	            lightboxPostCommands.reply(
	              actionDescriptors.replyCreate({
	                postId: Number(targetPost.id),
	                replyToPostNumber: Number(
	                  targetPost.post_number
	                )
	              })
	            )
	          )),
	          topicImages: features.topicImages,
	          ...lightboxOptions.originalSources ? { originalSources: lightboxOptions.originalSources } : {},
	          ...this.imageResources ? { imageResources: this.imageResources } : {},
	          ...this.imageDownloads ? { imageDownloads: this.imageDownloads } : {},
	          ...lightboxOptions.confirmOriginalDownload ? {
	            confirmOriginalDownload: lightboxOptions.confirmOriginalDownload
	          } : {
	            confirmOriginalDownload: (missing, total) => this.feedback.confirm({
	              title: total === 1 ? "下载当前图片" : "批量下载图片",
	              message: total === 1 ? "当前原图尚未缓存,是否按阅读器限速获取原图?" : `${missing} 张原图尚未缓存,是否按阅读器限速逐张获取?`,
	              note: "取消将使用每张图片当前最高缓存或预览质量。",
	              confirmLabel: "获取原图",
	              cancelLabel: "使用预览质量",
	              tone: "primary"
	            })
	          },
	          notify: (message) => this.feedback.show(message),
	          ...lightboxOptions.originalByDefault === void 0 ? {} : {
	            originalByDefault: lightboxOptions.originalByDefault
	          },
	          ...lightboxOptions.commentsExpandedByDefault === void 0 ? {} : {
	            commentsExpandedByDefault: lightboxOptions.commentsExpandedByDefault
	          },
	          ...lightboxOptions.descriptionExpandedByDefault === void 0 ? {} : {
	            descriptionExpandedByDefault: lightboxOptions.descriptionExpandedByDefault
	          },
	          ...lightboxOptions.readDefaults ? { readDefaults: lightboxOptions.readDefaults } : {},
	          ...lightboxOptions.preferences ? { preferences: lightboxOptions.preferences } : {},
	          ...lightboxOptions.commentsEnabled === void 0 ? {} : { commentsEnabled: lightboxOptions.commentsEnabled },
	          ...lightboxOptions.frameScheduler ? { frameScheduler: lightboxOptions.frameScheduler } : {},
	          onJumpToPost: async (item) => {
	            if (!value)
	              throw new Error("Topic context 尚未完成装配");
	            await value.topicNavigation.navigate({
	              postNumber: item.sourcePostNumber,
	              source: "lightbox",
	              alignment: "center",
	              highlight: !0
	            }), await lightboxOptions.onJumpToPost?.(item, value);
	          },
	          onClose: () => {
	            value && lightboxOptions.onClose?.(value);
	          },
	          parentScope: context.scope,
	          onError: (cause) => {
	            try {
	              lightboxOptions.onError?.(cause);
	            } catch {
	            }
	            reportTopicFeature(
	              context.topicId,
	              "image-interaction",
	              cause
	            );
	          }
	        }) : null, topicSummaryPreview = topicSummaryPreviews.get(context.scope), topicSummaryPreviewObjectUrls = options.resources?.objectUrls;
	        topicLightbox && topicSummaryPreview && topicSummaryPreviewObjectUrls && (topicSummaryPreview.open = ({ blob, alt, returnFocus }) => {
	          const source = topicSummaryPreviewObjectUrls.createObjectURL(blob);
	          try {
	            topicLightbox.open({
	              items: [Object.freeze({
	                key: `topic-summary-share:${context.topicId}:${Date.now()}`,
	                topicId: context.topicId,
	                sourcePostNumber: 1,
	                imageOrder: 0,
	                previewSrc: source,
	                originalSrc: source,
	                alt
	              })],
	              initialIndex: 0,
	              returnFocus,
	              commentsExpanded: !1,
	              descriptionExpanded: !1,
	              commentsEnabled: !1,
	              includeTopicImages: !1,
	              batchEnabled: !1
	            }).view.scope.add(() => {
	              topicSummaryPreviewObjectUrls.revokeObjectURL(source);
	            });
	          } catch (cause) {
	            throw topicSummaryPreviewObjectUrls.revokeObjectURL(source), cause;
	          }
	        }, context.scope.add(() => {
	          topicSummaryPreview.open = null;
	        }));
	        const openTopicImage = options.openTopicImage, topicImageInteraction = topicLightbox || openTopicImage ? new import_reader_topic_image_interaction.ReaderTopicImageInteraction({
	          topicHost: result.value.root,
	          additionalHosts: [
	            topicContextSurface.discussionContentHost
	          ],
	          images: features.topicImages,
	          open: (request) => {
	            if (topicLightbox) {
	              topicLightbox.open({
	                items: request.items,
	                initialIndex: request.initialIndex,
	                returnFocus: request.returnFocus,
	                ...request.commentsEnabled === void 0 ? {} : { commentsEnabled: request.commentsEnabled },
	                ...request.includeTopicImages === void 0 ? {} : { includeTopicImages: request.includeTopicImages }
	              });
	              return;
	            }
	            if (!value) throw new Error("Topic context 尚未完成装配");
	            return openTopicImage(request, value);
	          },
	          currentTopicId: context.topicId,
	          loadQuotedPost: (targetTopicId, targetPostNumber) => features.topicContext.loadQuotedPost(
	            targetTopicId,
	            targetPostNumber
	          ),
	          parentScope: context.scope,
	          onError: (cause) => reportTopicFeature(
	            context.topicId,
	            "image-interaction",
	            cause
	          )
	        }) : null, topicSelectionQuote = new import_reader_selection_quote_feature.ReaderSelectionQuoteFeature({
	          document: options.document,
	          root: this.shell.view.surfaceHost,
	          contentRoot: this.shell.view.modal,
	          topicId: context.topicId,
	          topic: () => result.value.services.session.topic ?? result.value.topic,
	          postById: (postId) => result.value.services.session.postById(postId),
	          postByNumber: (postNumber) => result.value.services.session.postByNumber(postNumber),
	          images: features.topicImages,
	          composer: this.composer,
	          ...options.share ? { clipboard: options.share } : {},
	          feedback: this.feedback,
	          parentScope: context.scope,
	          onError: (cause) => reportTopicFeature(
	            context.topicId,
	            "selection-quote",
	            cause
	          )
	        });
	        value = Object.freeze({
	          ...result.value,
	          topicImages: features.topicImages,
	          topicMedia: features.topicMedia,
	          topicCookedContent: features.topicCookedContent,
	          topicHeader,
	          topicHeaderView,
	          topicOnlyOp,
	          topicSpecialContent: features.topicSpecialContent,
	          topicContext: features.topicContext,
	          topicContextFeature: features.topicContextFeature,
	          topicContextSurface,
	          topicLiveNavigation,
	          topicLiveNavigationView,
	          topicNavigation,
	          topicFlow,
	          topicSelectionQuote,
	          topicActionRail: features.topicActionRail,
	          topicTimeline,
	          topicTimelineView,
	          topicImageInteraction,
	          topicLightbox
	        });
	        const readyCleanup = topicReady?.(value, context);
	        return typeof readyCleanup == "function" && context.scope.add(readyCleanup), Object.freeze({
	          ...result,
	          value,
	          prepareClose: async (reason) => {
	            await result.prepareClose?.(reason), this.#rememberHistoryTopicMetadata(value);
	          }
	        });
	      }, this.history = new import_reader_history_repository.ReaderHistoryRepository({
	        storage: options.storage,
	        authScope: options.topic.authScope,
	        ...options.history?.key === void 0 ? {} : { key: options.history.key },
	        ...options.history?.maxAgeMs === void 0 ? {} : { maxAgeMs: options.history.maxAgeMs },
	        ...options.history?.now === void 0 ? {} : { now: options.history.now }
	      }), this.history.load(), this.data.requests.changes.subscribe(
	        (snapshot) => this.#collectChronicleRequests(snapshot),
	        this.scope
	      ), this.#collectChronicleRequests(this.data.requests.snapshot), this.history.changes.subscribe(() => {
	        this.notificationPanelView?.syncArchiveMarkers(), this.bookmarkPanelView?.syncArchiveMarkers();
	      }, this.scope), this.historyNavigation = new import_reader_history_navigation_controller.ReaderHistoryNavigationController({
	        history: this.history,
	        readSortMode: options.history?.readSortMode ?? (() => "recent-viewed"),
	        port: {
	          activeTopicId: () => this.shell.activeTopicId,
	          captureAnchor: () => this.#captureAndRememberHistoryAnchor(),
	          openTopic: async (topicId) => {
	            const result = await this.shell.open(
	              topicId,
	              this.topicFactory
	            );
	            return (result.status === "opened" || result.status === "reused") && this.#rememberHistoryTopicMetadata(result.value), this.#historyOpenResult(result);
	          },
	          restoreAnchor: async (topicId, anchor, restoreOptions) => {
	            const value = this.shell.activeValue;
	            if (this.shell.activeTopicId !== topicId || value === null)
	              throw new Error(
	                `历史目标 Topic ${topicId} 未处于 active 状态`
	              );
	            const proportional = anchor.viewport.scrollRatio !== void 0, restoreSemanticState = restoreOptions?.restoreSemanticState !== !1;
	            if (!proportional && restoreSemanticState) {
	              const result = await value.topicNavigation.navigate({
	                postNumber: anchor.viewport.postNumber,
	                source: "history",
	                alignment: "nearest",
	                highlight: restoreOptions?.highlight !== !1
	              });
	              if (result.status !== "revealed")
	                throw new Error(
	                  `旧历史楼层 #${anchor.viewport.postNumber} 恢复失败:${result.status}`
	                );
	            }
	            const navigationRevision = value.topicNavigation.revision;
	            if (restoreSemanticState && anchor.replyWindow ? await value.topicContextSurface.restoreDiscussionState(anchor.replyWindow) : value.topicContext.closeDiscussion(), !value.topicNavigation.isCurrent(navigationRevision)) return;
	            const quoteHighlight = restoreSemanticState ? anchor.quoteHighlight : null;
	            if (!await value.topicContextFeature.restoreQuoteHighlightState(quoteHighlight))
	              throw new Error(
	                `历史引用高亮 #${quoteHighlight?.postNumber ?? 0} 恢复失败`
	              );
	            if (!(quoteHighlight === null && !value.topicNavigation.isCurrent(navigationRevision))) {
	              if ((proportional || restoreSemanticState) && !value.dom.restoreViewportAnchor(anchor.viewport))
	                throw new Error("历史 Reader 高度锚点恢复失败");
	              !proportional && restoreSemanticState && value.topicTimeline.syncVisiblePost(anchor.viewport.postNumber), proportional ? this.#rememberHistoryTopic(
	                value,
	                anchor.viewport.postNumber,
	                anchor.viewport
	              ) : this.#rememberHistoryTopicMetadata(value);
	            }
	          }
	        },
	        parentScope: this.scope,
	        onError: (cause) => reportTopicFeature(
	          this.shell.activeTopicId ?? 0,
	          "history",
	          cause
	        )
	      });
	      const historyViewOptions = options.history?.navigationView;
	      if (historyViewOptions) {
	        const root = this.shell.view.root, backEdge = root.querySelector(
	          ".ldp-reader-history-edge-back"
	        ), forwardEdge = root.querySelector(
	          ".ldp-reader-history-edge-forward"
	        ), backButton = root.querySelector(
	          ".ldp-reader-history-back"
	        ), forwardButton = root.querySelector(
	          ".ldp-reader-history-forward"
	        );
	        if (!backEdge || !forwardEdge || !backButton || !forwardButton)
	          throw new Error(
	            "历史导航 View 缺少 Shell 命名边缘或按钮"
	          );
	        this.historyNavigationView = new import_reader_history_navigation_view.ReaderHistoryNavigationView({
	          navigation: this.historyNavigation,
	          elements: {
	            root,
	            modal: this.shell.view.modal,
	            backEdge,
	            forwardEdge,
	            backButton,
	            forwardButton
	          },
	          preferences: historyViewOptions.preferences,
	          window: historyViewOptions.window === void 0 ? options.document.defaultView : historyViewOptions.window,
	          topicTitle: (topicId) => this.history.entry(topicId)?.title ?? null,
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "history",
	            cause
	          )
	        });
	      } else
	        this.historyNavigationView = null;
	      const historyPanelOptions = options.history?.panelView;
	      if (historyPanelOptions) {
	        const root = this.shell.view.root, toggle = root.querySelector(
	          ".ldp-history-toggle"
	        ), popover = root.querySelector(
	          ".ldp-history-popover"
	        ), sortToggle = root.querySelector(
	          ".ldp-history-sort-toggle"
	        ), multiButton = root.querySelector(
	          ".ldp-history-multi"
	        ), clearButton = root.querySelector(
	          ".ldp-history-clear"
	        ), defaultActions = root.querySelector(
	          ".ldp-history-default-actions"
	        ), bulkActions = root.querySelector(
	          ".ldp-history-bulk-actions"
	        ), selectScope = root.querySelector(
	          ".ldp-history-select-scope"
	        ), selectToggle = root.querySelector(
	          ".ldp-history-select-toggle"
	        ), deleteSelected = root.querySelector(
	          ".ldp-history-delete-selected"
	        ), deleteSelectedLabel = root.querySelector(
	          ".ldp-history-delete-selected-label"
	        ), multiDone = root.querySelector(
	          ".ldp-history-multi-done"
	        ), search = root.querySelector(
	          ".ldp-history-search"
	        ), searchClear = root.querySelector(
	          ".ldp-history-search-clear"
	        ), categoryFilter = root.querySelector(
	          ".ldp-history-category-filter"
	        ), tagFilter = root.querySelector(
	          ".ldp-history-tag-filter"
	        ), list = root.querySelector(
	          ".ldp-history-list"
	        ), pagePrevious = root.querySelector(
	          ".ldp-history-page-prev"
	        ), pageInfo = root.querySelector(
	          ".ldp-history-page-info"
	        ), pageNext = root.querySelector(
	          ".ldp-history-page-next"
	        );
	        if (!toggle || !popover || !sortToggle || !multiButton || !clearButton || !defaultActions || !bulkActions || !selectScope || !selectToggle || !deleteSelected || !deleteSelectedLabel || !multiDone || !search || !searchClear || !categoryFilter || !tagFilter || !list || !pagePrevious || !pageInfo || !pageNext)
	          throw new Error(
	            "历史列表 View 缺少 Shell 命名面板控件"
	          );
	        this.historyPanelView = new import_reader_history_panel_view.ReaderHistoryPanelView({
	          ...historyPanelOptions,
	          ...(historyPanelOptions.searchForms ?? options.searchForms) === void 0 ? {} : {
	            searchForms: historyPanelOptions.searchForms ?? options.searchForms
	          },
	          confirmDelete: historyPanelOptions.confirmDelete ?? ((request) => this.feedback.confirm(request)),
	          notify: historyPanelOptions.notify ?? ((message) => this.feedback.show(message)),
	          document: options.document,
	          mount: this.shell.view.surfaceHost,
	          storage: options.storage,
	          history: this.history,
	          elements: {
	            root,
	            toggle,
	            popover,
	            sortToggle,
	            multiButton,
	            clearButton,
	            defaultActions,
	            bulkActions,
	            selectScope,
	            selectToggle,
	            deleteSelected,
	            deleteSelectedLabel,
	            multiDone,
	            search,
	            searchClear,
	            categoryFilter,
	            tagFilter,
	            list,
	            pagePrevious,
	            pageInfo,
	            pageNext
	          },
	          openEntry: async (entry) => {
	            await this.#openHistoryEntry(entry);
	          },
	          parentScope: this.scope,
	          onError: (cause) => reportTopicFeature(
	            this.shell.activeTopicId ?? 0,
	            "history",
	            cause
	          )
	        });
	      } else
	        this.historyPanelView = null;
	      this.shell.changes.subscribe((state) => {
	        (state === "switching" || state === "closed") && this.#closeApplicationSurfaces();
	      }, this.scope);
	    } catch (error) {
	      throw this.scope.destroy(), error;
	    }
	  }
	  get performance() {
	    return this.#performance;
	  }
	  applyPerformance(snapshot) {
	    if (this.#destroyed || this.scope.destroyed) return;
	    this.#performance = snapshot, this.#applyPerformanceInfrastructure();
	    const active = this.shell.activeValue;
	    active?.services.session.applyPageSize(snapshot.pageSize), active?.topicFlow.refreshPerformance();
	  }
	  async open(topicId) {
	    return (await this.openTarget({
	      topicId,
	      source: "link"
	    })).topic;
	  }
	  async openTarget(request) {
	    if (this.#destroyed || this.scope.destroyed)
	      return Object.freeze({
	        topic: Object.freeze({
	          status: "failed",
	          topicId: (0, import_identifiers.discourseTopicId)(request.topicId),
	          cause: new Error("ReaderBrowserRuntime 已销毁")
	        }),
	        navigation: null
	      });
	    this.#boostTargetHighlight.clear();
	    const normalizedTopicId = (0, import_identifiers.discourseTopicId)(request.topicId), chronicleRequestFloor = this.data.requests.snapshot.events.at(-1)?.id ?? 0;
	    this.#openRecoveryController?.abort(
	      new DOMException("新的打开事务已开始", "AbortError")
	    );
	    const recoveryController = new AbortController();
	    this.#openRecoveryController = recoveryController;
	    const transactionIsCurrent = () => !this.#destroyed && !this.scope.destroyed && !recoveryController.signal.aborted && this.#openRecoveryController === recoveryController, superseded = () => Object.freeze({
	      topic: Object.freeze({
	        status: "superseded",
	        topicId: normalizedTopicId
	      }),
	      navigation: null
	    }), releaseLoading = this.#loadingProgress?.begin(
	      Number(normalizedTopicId),
	      request.postNumber
	    );
	    try {
	      this.recovery.clear();
	      const previousTopicId = this.shell.activeTopicId !== null && this.shell.activeTopicId !== normalizedTopicId ? this.shell.activeTopicId : null, previousAnchor = previousTopicId === null ? null : this.historyNavigation.captureCurrent();
	      let result;
	      for (let attempt = 0; ; attempt += 1) {
	        if (result = await this.shell.open(
	          normalizedTopicId,
	          this.topicFactory
	        ), !transactionIsCurrent()) return superseded();
	        if (result.status !== "failed" || !readerShellOpenRetryable(result.cause) || attempt >= 2)
	          break;
	        this.feedback.show(
	          `帖子加载暂时失败,${attempt + 1} 秒后自动重试一次`
	        );
	        try {
	          await this.#openRetryDelay(
	            (attempt + 1) * 1e3,
	            recoveryController.signal
	          );
	        } catch {
	          return superseded();
	        }
	      }
	      if (result.status === "opened" || result.status === "reused") {
	        this.#lastFailedRequest = null, this.#rememberHistoryTopicMetadata(result.value), this.historyNavigation.snapshot.activeTopicId !== result.topicId && this.historyNavigation.activate(result.topicId);
	        let navigation = null;
	        try {
	          if (request.postNumber !== void 0) {
	            request.localArchive && await result.value.services.session.restoreUnavailablePostFromCache(
	              request.postNumber,
	              request.localArchive.status,
	              request.localArchive.confirmedAt,
	              request.localArchive.requestPath
	            );
	            for (let attempt = 0; ; attempt += 1) {
	              let navigationCause = null;
	              try {
	                navigation = await result.value.topicNavigation.navigate({
	                  postNumber: request.postNumber,
	                  source: request.source,
	                  ...request.alignment === void 0 ? {} : { alignment: request.alignment },
	                  ...request.focus === void 0 ? {} : { focus: request.focus },
	                  ...request.highlight === void 0 ? {} : { highlight: request.highlight },
	                  ...request.forceRefresh === void 0 ? {} : { forceRefresh: request.forceRefresh },
	                  ...request.cachedOnly === void 0 ? {} : { cachedOnly: request.cachedOnly },
	                  ...request.revealAsFloor === void 0 ? {} : { revealAsFloor: request.revealAsFloor }
	                });
	              } catch (cause) {
	                navigationCause = cause;
	              }
	              if (!transactionIsCurrent()) return superseded();
	              if (!(navigationCause !== null ? readerShellOpenRetryable(navigationCause) : navigation?.status === "unresolved-tree") || attempt >= 2) {
	                if (navigationCause !== null)
	                  return this.#lastFailedRequest = Object.freeze({ ...request }), this.feedback.show(
	                    "帖子已打开,但目标楼层定位失败;当前 Topic 已保留,可稍后再次跳转"
	                  ), Object.freeze({ topic: result, navigation: null });
	                navigation?.status === "unresolved-tree" && this.feedback.show(
	                  "帖子已打开,但目标楼层的回复树暂未完成挂载;当前 Topic 已保留,可稍后再次跳转"
	                );
	                break;
	              }
	              this.feedback.show(
	                `目标楼层定位暂时失败,${attempt + 1} 秒后自动重试一次`
	              );
	              try {
	                await this.#openRetryDelay(
	                  (attempt + 1) * 1e3,
	                  recoveryController.signal
	                );
	              } catch {
	                return superseded();
	              }
	            }
	            if (!transactionIsCurrent()) return superseded();
	            if (navigation?.status !== "revealed")
	              return Object.freeze({ topic: result, navigation });
	            const targetPost = result.value.services.session.postByNumber(
	              request.postNumber
	            ), treeParent = result.value.replies.topology.parentOf(
	              request.postNumber
	            ), canonicalParent = treeParent === void 0 ? (0, import_identifiers.tryDiscoursePostNumber)(
	              targetPost?.reply_to_post_number
	            ) : treeParent;
	            if (request.revealAsFloor !== !0 && canonicalParent !== null && canonicalParent !== void 0 && canonicalParent > 1 && !navigation.element?.closest(
	              ".ldp-descendant-replies-layer"
	            ) && (await result.value.topicContext.openDiscussion(
	              request.postNumber
	            ), !transactionIsCurrent()))
	              return superseded();
	            request.quoteHighlight && (navigation.element && result.value.topicContextFeature.applyRevealedQuoteHighlight(
	              request.quoteHighlight,
	              navigation.element
	            ) || this.feedback.show(
	              `目的地内容已修改;已定位到楼层 #${request.postNumber}`
	            )), request.highlight !== !1 && request.boostId !== void 0 && this.#highlightBoostTarget(request, navigation.element);
	          }
	          return Object.freeze({ topic: result, navigation });
	        } finally {
	          transactionIsCurrent() && (this.#rememberHistoryTopicMetadata(result.value), this.historyNavigation.snapshot.activeTopicId !== result.topicId && this.historyNavigation.activate(result.topicId));
	        }
	      }
	      if (result.status === "failed") {
	        if (!transactionIsCurrent()) return superseded();
	        this.#lastFailedRequest = Object.freeze({ ...request }), readerShellFailureKind(result.cause) === "cloudflare" && (await this.permit.noteCloudflareChallenge({
	          href: this.#challengeHref,
	          force: !0
	        }), await this.rateLimitNotice.refresh());
	        let previousRestored = previousTopicId !== null && this.shell.activeTopicId === previousTopicId;
	        if (previousTopicId !== null && !previousRestored && this.shell.activeTopicId === null) {
	          const previous = await this.shell.open(
	            previousTopicId,
	            this.topicFactory
	          );
	          if (!transactionIsCurrent()) return superseded();
	          if (previousRestored = previous.status === "opened" || previous.status === "reused", previousRestored && previousAnchor)
	            try {
	              await this.historyNavigation.restore(
	                previousTopicId,
	                previousAnchor
	              );
	            } catch {
	              this.feedback.show(
	                "原帖子已恢复,但之前的阅读位置未能完整还原"
	              );
	            }
	        }
	        previousRestored ? this.feedback.show("切换帖子失败,已保留当前帖子") : this.recovery.show(
	          readerShellRecoveryFailure(
	            result.cause,
	            this.#challengeHref
	          )
	        );
	      }
	      return Object.freeze({ topic: result, navigation: null });
	    } finally {
	      this.#rememberBoostChronicleRequest(request, chronicleRequestFloor), this.#openRecoveryController === recoveryController && (this.#openRecoveryController = null), releaseLoading?.();
	    }
	  }
	  #highlightBoostTarget(request, navigationElement) {
	    const boostId = Number(request.boostId), postNumber = Number(request.postNumber);
	    if (!Number.isSafeInteger(boostId) || boostId <= 0 || !Number.isSafeInteger(postNumber) || postNumber <= 0) return;
	    const roots = /* @__PURE__ */ new Set(), navigationRoot = navigationElement?.matches(".ldp-post") ? navigationElement : navigationElement?.closest(".ldp-post");
	    navigationRoot?.isConnected && roots.add(navigationRoot);
	    for (const root of this.shell.view.root.querySelectorAll(
	      `.ldp-post[data-post-number="${postNumber}"]`
	    ))
	      roots.add(root);
	    const visibleCandidates = [];
	    let hiddenFallback = null;
	    for (const root of roots)
	      for (const bubble of root.querySelectorAll(
	        ".ldp-boost-bubble[data-boost-id]"
	      ))
	        bubble.closest(".ldp-post") !== root || Number(bubble.dataset.boostId) !== boostId || (bubble.closest('[hidden],[aria-hidden="true"]') ? hiddenFallback ??= bubble : visibleCandidates.push(bubble));
	    const target = visibleCandidates.find((bubble) => bubble.closest(".ldp-descendant-replies-layer")) ?? visibleCandidates.find((bubble) => bubble.closest(".ldp-post") === navigationRoot) ?? visibleCandidates[0] ?? hiddenFallback;
	    target && this.#boostTargetHighlight.highlight(target);
	  }
	  async #recoverAvatarSource(source) {
	    return this.imageResources?.resolveAvatarSource(source) ?? "";
	  }
	  async close() {
	    return this.#boostTargetHighlight.clear(), this.#openRecoveryController?.abort(
	      new DOMException("Reader 已关闭", "AbortError")
	    ), this.#openRecoveryController = null, this.#lastFailedRequest = null, this.recovery.clear(), this.historyNavigation.captureCurrent(), this.#closeApplicationSurfaces(), this.shell.closeTopic();
	  }
	  async #restoreQuoteSource(source) {
	    if (this.#destroyed || this.scope.destroyed) return !1;
	    const topicId = (0, import_identifiers.discourseTopicId)(source.topicId), anchor = source.anchor === null ? null : (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(source.anchor), active = this.shell.activeValue;
	    if (this.shell.activeTopicId === topicId && active) {
	      if (anchor) {
	        await this.historyNavigation.restore(topicId, anchor, {
	          highlight: !1
	        });
	        const restored2 = this.shell.activeValue;
	        return !restored2 || !this.#restoreExactQuoteViewport(restored2, anchor) ? !1 : (this.#highlightQuoteSource(restored2, source), !0);
	      }
	      return (await active.topicNavigation.navigate({
	        postNumber: source.postNumber,
	        source: "quote",
	        alignment: "nearest",
	        highlight: !0
	      })).status === "revealed";
	    }
	    const opened = await this.openTarget({
	      topicId,
	      postNumber: anchor?.viewport.postNumber ?? source.postNumber,
	      source: "quote",
	      alignment: "nearest",
	      highlight: anchor === null
	    });
	    if (opened.topic.status !== "opened" && opened.topic.status !== "reused")
	      return !1;
	    if (!anchor) return opened.navigation?.status === "revealed";
	    await this.historyNavigation.restore(topicId, anchor, {
	      highlight: !1
	    });
	    const restored = this.shell.activeValue;
	    return !restored || !this.#restoreExactQuoteViewport(restored, anchor) ? !1 : (this.#highlightQuoteSource(restored, source), !0);
	  }
	  #restoreExactQuoteViewport(value, anchor) {
	    return value.dom.captureViewportAnchor()?.postNumber === anchor.viewport.postNumber ? !0 : value.dom.restoreViewportAnchor({
	      postNumber: anchor.viewport.postNumber,
	      postOffset: anchor.viewport.postOffset,
	      scrollTop: anchor.viewport.scrollTop
	    });
	  }
	  #highlightQuoteSource(value, source) {
	    source.anchor?.replyWindow && value.topicContextSurface.highlightDiscussionPost(source.postNumber) || value.dom.highlightPost(source.postNumber);
	  }
	  readerSurfaceOpen() {
	    if (this.shell.view.root.hidden) return !1;
	    if (this.actionSurfaces.active || this.userCardView.isOpen || this.userMediaViewer?.activeRoot?.isConnected) return !0;
	    const document = this.shell.view.root.ownerDocument;
	    return (0, import_reader_native_composer_window.visibleDiscourseNativeFloatingSurface)(document) ? !0 : (0, import_reader_escape_surface.readerFrontmostEscapeSurface)(document) !== null;
	  }
	  readerExitBlocked() {
	    return this.composer.isOpen() || this.readerSurfaceOpen();
	  }
	  readerShortcutContextBlocked() {
	    if (this.composer.isOpen()) return !0;
	    const document = this.shell.view.root.ownerDocument;
	    return !!(0, import_reader_escape_surface.readerSurfaceQuery)(document, [
	      ".ldp-settings-popover:not([hidden])",
	      ".ldp-reader-action-layer:not([hidden])",
	      ".ldp-lightbox",
	      ".ldp-code-preview-layer"
	    ].join(","));
	  }
	  handleCloseReaderShortcut(event) {
	    if ((0, import_reader_shortcut_controller.readerShortcutBindingFromEvent)(event) !== "Escape") {
	      if (this.readerShortcutContextBlocked())
	        return event.type !== "keydown";
	      if (this.readerExitBlocked())
	        return this.#dispatchSurfaceCloseEscape(), !0;
	    }
	    return this.closeExpandedReply() || this.close();
	  }
	  closeExpandedReply() {
	    const postNumber = this.shell.activeValue?.topicContextFeature.collapseExpandedDefaultPost() ?? null;
	    return postNumber ? (this.feedback.show(`已收起楼层 #${postNumber}`), !0) : !1;
	  }
	  #closeApplicationSurfaces() {
	    this.userMediaViewer?.close(), this.userCardView.close(), this.actionSurfaces.closeActive();
	  }
	  #dispatchSurfaceCloseEscape() {
	    const document = this.shell.view.root.ownerDocument, window = document.defaultView;
	    if (!window) return;
	    let event;
	    typeof window.KeyboardEvent == "function" ? event = new window.KeyboardEvent("keydown", {
	      key: "Escape",
	      code: "Escape",
	      bubbles: !0,
	      cancelable: !0
	    }) : (event = new window.Event("keydown", {
	      bubbles: !0,
	      cancelable: !0
	    }), Object.defineProperties(event, {
	      key: { value: "Escape", configurable: !0 },
	      code: { value: "Escape", configurable: !0 }
	    })), readerSurfaceOnlyCloseEvents.add(event), document.dispatchEvent(event);
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.#openRecoveryController?.abort(
	      new DOMException("Reader runtime 已销毁", "AbortError")
	    ), this.#openRecoveryController = null, this.#lastFailedRequest = null, this.historyNavigation.captureCurrent(), this.scope.destroy());
	  }
	  #openManualCloudflareChallenge(href, observationUsername = "") {
	    if (this.scope.destroyed) return;
	    if (this.#manualChallengePromise) {
	      this.permit.resolveCloudflareChallenge({
	        href,
	        signal: this.#manualChallengeController.signal,
	        focus: !0
	      }).catch(() => {
	      }), observationUsername && this.#manualChallengePromise.then((passed) => {
	        passed && !this.scope.destroyed && this.userObservations.retry(observationUsername);
	      }).catch(() => {
	      });
	      return;
	    }
	    const failedRequest = this.#lastFailedRequest, promise = this.permit.resolveCloudflareChallenge({
	      href,
	      signal: this.#manualChallengeController.signal,
	      focus: !0
	    }).then(async (passed) => {
	      if (this.scope.destroyed) return passed;
	      let retried = !1, recovered = !1;
	      if (passed) {
	        if (await this.data.client.resetRateLimits(), failedRequest !== null && this.#lastFailedRequest === failedRequest) {
	          retried = !0;
	          const result = await this.openTarget({
	            ...failedRequest,
	            forceRefresh: !0
	          });
	          recovered = result.topic.status === "opened" || result.topic.status === "reused";
	        }
	        observationUsername ? this.userObservations.retry(observationUsername) : (!retried || recovered) && this.userObservations.resumeRecoverable("cloudflare-challenge"), await this.rateLimitNotice.refresh();
	      }
	      return this.feedback.show(
	        passed ? observationUsername ? `Cloudflare 验证已通过,@${observationUsername} 已从断点继续` : retried ? recovered ? "Cloudflare 验证已通过,目标帖子已继续加载" : "验证会话已通过,但目标帖子仍被限制;未继续追发请求" : "Cloudflare 验证已通过,请求已回到原有有序管线" : "验证浮窗未完成;请允许弹出窗口后重试"
	      ), passed;
	    }).catch((error) => {
	      throw this.#manualChallengeController.signal.aborted || this.feedback.show("Cloudflare 验证未完成,请稍后重试"), error;
	    }).finally(() => {
	      this.#manualChallengePromise === promise && (this.#manualChallengePromise = null);
	    });
	    this.#manualChallengePromise = promise, promise.catch(() => {
	    });
	  }
	  #applyPerformanceInfrastructure() {
	    const snapshot = this.#performance;
	    this.permit.applyRuntimePolicy({
	      shortBudget: snapshot.requestShortBudget,
	      longBudget: snapshot.requestLongBudget,
	      minIntervalMs: snapshot.requestMinIntervalMs,
	      maxConcurrent: snapshot.requestMaxConcurrent
	    }), this.data.applyRequestRuntimePolicy({
	      maxConcurrent: snapshot.requestMaxConcurrent
	    });
	  }
	  #historyArchiveMarker(topicId, postNumber) {
	    return this.history?.archiveMarker(topicId, postNumber) ?? null;
	  }
	  #captureAndRememberHistoryAnchor() {
	    const anchor = this.#captureHistoryAnchor(), value = this.shell.activeValue;
	    return anchor && value && this.#rememberHistoryTopic(
	      value,
	      anchor.viewport.postNumber,
	      anchor.viewport
	    ), anchor;
	  }
	  #captureHistoryAnchor() {
	    const value = this.shell.activeValue;
	    if (!value) return null;
	    const viewport = value.dom.captureViewportAnchor();
	    return viewport ? (0, import_reader_history_model.normalizeReaderHistoryAnchorState)({
	      viewport,
	      replyWindow: value.topicContextSurface.captureDiscussionState(),
	      quoteHighlight: value.topicContextFeature.captureQuoteHighlightState()
	    }) : null;
	  }
	  #collectChronicleRequests(snapshot) {
	    const retained = new Set(snapshot.events.map((event) => event.id));
	    for (const id of this.#chronicleRequestIds)
	      retained.has(id) || this.#chronicleRequestIds.delete(id);
	    for (const event of snapshot.events) {
	      if (this.#chronicleRequestIds.has(event.id) || event.pending) continue;
	      if (event.phase !== "finished" || event.status !== 404 || !event.sameOrigin) {
	        this.#chronicleRequestIds.add(event.id);
	        continue;
	      }
	      const target = (0, import_reader_chronicle_repository.readerChronicleRequestTarget)(event.path);
	      if (!target) {
	        this.#chronicleRequestIds.add(event.id);
	        continue;
	      }
	      const input = this.#chronicleInput(target, event);
	      if (input)
	        try {
	          this.chronicle.remember(input);
	        } catch {
	        } finally {
	          this.#chronicleRequestIds.add(event.id);
	        }
	    }
	  }
	  #rememberChronicleArchives(value) {
	    const session = value.services.session, topicId = Number(session.topicId);
	    if (!Number.isSafeInteger(topicId) || topicId <= 0) return;
	    const topic = session.topic ?? value.topic, topicTitle = String(topic?.title ?? "").trim() || this.history.entry(topicId)?.title || `帖子 #${topicId}`, alreadyRemembered = (kind, postNumber) => this.chronicle.snapshot.records.some(
	      (record) => record.kind === kind && Number(record.topicId) === topicId && (kind === "topic" || Number(record.postNumber) === postNumber)
	    ), remember = (kind, postNumber, confirmedAt) => {
	      if (alreadyRemembered(kind, postNumber)) return;
	      const post = postNumber === null ? session.postByNumber(1) : session.postByNumber(postNumber);
	      if (kind === "reply" && (!post || post.reader_local_archive_placeholder === !0) || kind === "topic" && !topic && !session.cachedPosts().length) return;
	      const rawPostId = Number(post?.id);
	      try {
	        this.chronicle.remember({
	          kind,
	          status: 404,
	          bodyCached: !0,
	          topicId,
	          topicTitle,
	          ...postNumber === null ? {} : { postNumber },
	          ...Number.isSafeInteger(rawPostId) && rawPostId > 0 ? { postId: rawPostId } : {},
	          requestPath: postNumber === null ? `/t/${topicId}.json` : `/posts/by_number/${topicId}/${postNumber}.json`,
	          requestMethod: "GET",
	          requestSource: "reader",
	          callSite: "topic-local-archive",
	          observedAt: confirmedAt
	        });
	      } catch {
	      }
	    }, archive = session.localArchiveState();
	    archive.topic?.status === 404 && remember("topic", null, archive.topic.confirmedAt);
	    for (const entry of archive.posts)
	      entry.status === 404 && remember("reply", Number(entry.postNumber), entry.confirmedAt);
	  }
	  #rememberBoostChronicleRequest(request, requestFloor) {
	    const boostId = Number(request.boostId);
	    if (!Number.isSafeInteger(boostId) || boostId <= 0) return;
	    const resolvedBoost = this.#chronicleBoostTarget(boostId);
	    for (const event of this.data.requests.snapshot.events) {
	      if (event.id <= requestFloor || event.phase !== "finished" || event.status !== 404 || !event.sameOrigin) continue;
	      const target = (0, import_reader_chronicle_repository.readerChronicleRequestTarget)(event.path);
	      if (!target || target.kind === "boost") continue;
	      const resolvedRequest = this.#chronicleInput(target, event);
	      if (!(!resolvedRequest || Number(resolvedRequest.topicId) !== request.topicId))
	        try {
	          this.chronicle.remember({
	            kind: "boost",
	            status: 404,
	            bodyCached: !0,
	            topicId: request.topicId,
	            topicTitle: resolvedBoost?.topicTitle || this.history.entry(request.topicId)?.title,
	            postNumber: resolvedBoost?.postNumber ?? request.postNumber,
	            postId: resolvedBoost?.postId,
	            boostId,
	            requestPath: event.path,
	            requestMethod: event.method,
	            requestSource: event.source,
	            callSite: [
	              `boost-target:${request.source}`,
	              event.callSite
	            ].filter(Boolean).join(" · "),
	            observedAt: event.endedAt || event.startedAt
	          });
	        } catch {
	        }
	    }
	  }
	  #chronicleInput(target, event) {
	    let targetKind = target.kind, topicId = target.topicId, postNumber = target.postNumber, postId = target.postId;
	    const boostId = target.boostId;
	    let topicTitle = "";
	    const active = this.shell.activeValue;
	    if (postId !== null && active) {
	      const post = active.services.session.postById(postId), resolved = (0, import_identifiers.tryDiscoursePostNumber)(post?.post_number);
	      resolved !== null && (topicId = Number(active.services.session.topicId), postNumber = resolved, targetKind = resolved === 1 ? "topic" : "reply");
	    }
	    if (targetKind === "boost" && boostId !== null) {
	      const resolved = this.#chronicleBoostTarget(boostId);
	      resolved && (topicId = resolved.topicId, postNumber = resolved.postNumber, postId = resolved.postId, topicTitle = resolved.topicTitle);
	    }
	    if (topicId === null && postId !== null) {
	      const activity = this.bookmarkController?.activitySyncRecords().find(
	        (entry) => entry.postId === postId
	      );
	      activity && (topicId = Number(activity.topicId), postNumber = Number(activity.postNumber), topicTitle = activity.title, targetKind = postNumber === 1 ? "topic" : "reply");
	    }
	    if (topicId === null || targetKind === "reply" && postNumber === null && postId === null || targetKind === "boost" && boostId === null) return null;
	    if (!topicTitle && active && Number(active.services.session.topicId) === topicId) {
	      const topic = active.services.session.topic ?? active.topic;
	      topicTitle = String(topic.title ?? "").trim();
	    }
	    topicTitle ||= this.history.entry(topicId)?.title ?? `帖子 #${topicId}`;
	    const cachedPost = active && Number(active.services.session.topicId) === topicId ? postNumber !== null ? active.services.session.postByNumber(postNumber) : postId !== null ? active.services.session.postById(postId) : targetKind === "topic" ? active.services.session.postByNumber(1) : void 0 : void 0;
	    return !cachedPost || cachedPost.reader_local_archive_placeholder === !0 ? null : Object.freeze({
	      kind: targetKind,
	      status: 404,
	      bodyCached: !0,
	      topicId,
	      topicTitle,
	      ...postNumber === null ? {} : { postNumber },
	      ...postId === null ? {} : { postId },
	      ...boostId === null ? {} : { boostId },
	      requestPath: event.path,
	      requestMethod: event.method,
	      requestSource: event.source,
	      callSite: event.callSite,
	      observedAt: event.endedAt || event.startedAt
	    });
	  }
	  #chronicleBoostTarget(boostId) {
	    const active = this.shell.activeValue;
	    if (active)
	      for (const post of active.services.session.cachedPosts()) {
	        const source = post;
	        if (!(Array.isArray(source.boosts) ? source.boosts : source.boosts ? [source.boosts] : []).some((value) => value !== null && typeof value == "object" && Number(value.id) === boostId)) continue;
	        const topic = active.services.session.topic ?? active.topic;
	        return Object.freeze({
	          topicId: Number(active.services.session.topicId),
	          postNumber: Number(post.post_number),
	          postId: Number.isSafeInteger(Number(post.id)) ? Number(post.id) : null,
	          topicTitle: String(topic.title ?? "").trim()
	        });
	      }
	    const activity = this.bookmarkController?.activitySyncRecords().find(
	      (entry) => entry.tab === "Boost" && entry.identity === `boost:${boostId}`
	    );
	    if (activity)
	      return Object.freeze({
	        topicId: Number(activity.topicId),
	        postNumber: Number(activity.postNumber),
	        postId: activity.postId === null ? null : Number(activity.postId),
	        topicTitle: activity.title
	      });
	    const notification = this.notificationController?.syncHistoryRecords().find(
	      (entry) => entry.group === "boosts" && entry.identity === `boosts:${boostId}`
	    );
	    return notification?.target ? Object.freeze({
	      topicId: Number(notification.target.topicId),
	      postNumber: Number(notification.target.postNumber),
	      postId: null,
	      topicTitle: this.history.entry(notification.target.topicId)?.title ?? `帖子 #${notification.target.topicId}`
	    }) : null;
	  }
	  #rememberHistoryTopicMetadata(value) {
	    this.#rememberHistoryTopic(
	      value,
	      this.history.entry(value.services.session.topicId)?.postNumber ?? 1
	    );
	  }
	  #rememberHistoryTopic(value, postNumber = value.topicTimeline.snapshot.currentPostNumber, viewport) {
	    const topic = value.services.session.topic ?? value.topic, topicHeader = value.topicHeader.snapshot, posts = value.services.session.cachedPosts(), firstPost = posts.find(
	      (post) => (0, import_identifiers.tryDiscoursePostNumber)(post.post_number) === 1
	    ), topicObservationMetadata = (0, import_reader_user_observation_model.normalizeReaderUserTopicMetadata)(
	      value.services.session.topicId,
	      topic,
	      firstPost
	    ), headerObservationMetadata = (0, import_reader_user_observation_model.normalizeReaderUserTopicMetadata)(
	      value.services.session.topicId,
	      Object.freeze({
	        title: topicHeader.title,
	        category_id: topicHeader.categoryId || null,
	        category_name: topicHeader.category?.name ?? "",
	        tags: Object.freeze(topicHeader.tags.map((tag) => tag.name))
	      })
	    ), observationMetadata = topicObservationMetadata && headerObservationMetadata ? (0, import_reader_user_observation_model.mergeReaderUserTopicMetadata)(
	      topicObservationMetadata,
	      headerObservationMetadata
	    ) : topicObservationMetadata ?? headerObservationMetadata;
	    observationMetadata && this.userObservations.rememberTopicMetadata(observationMetadata);
	    const readPostNumbers = posts.filter(
	      (post) => post.read === !0
	    ).map((post) => post.post_number);
	    readPostNumbers.push(
	      ...value.services.read.snapshot().confirmed
	    );
	    const archive = value.services.session.localArchiveState(), floorArchive = archive.posts.find(
	      (entry) => entry.postNumber === postNumber
	    ) ?? null;
	    try {
	      this.history.remember({
	        topicId: value.services.session.topicId,
	        title: topicHeader.title || topic.title,
	        postsCount: Math.max(
	          Number(topic.posts_count) || 0,
	          Number(topic.highest_post_number) || 0
	        ),
	        avatarTemplate: topic.details?.created_by?.avatar_template ?? firstPost?.avatar_template,
	        ownerUsername: topic.details?.created_by?.username ?? firstPost?.username,
	        topicSubtitle: topicHeader.statsText === "主题信息暂不可用" ? "" : topicHeader.statsText,
	        categoryId: topicHeader.categoryId || null,
	        categoryName: topicHeader.category?.name ?? "",
	        tags: topicHeader.tags.map((tag) => tag.name),
	        ...viewport === void 0 ? {} : { viewport },
	        postNumber,
	        readPostNumbers,
	        archiveStatus: archive.topic?.status ?? floorArchive?.status ?? null,
	        archivePostNumber: archive.topic === null ? floorArchive?.postNumber ?? null : null
	      });
	    } catch {
	    }
	  }
	  #historyOpenResult(result) {
	    return result.status === "failed" ? Object.freeze({
	      status: "failed",
	      topicId: result.topicId,
	      cause: result.cause
	    }) : Object.freeze({
	      status: result.status,
	      topicId: result.topicId
	    });
	  }
	  async #openHistoryEntry(entry) {
	    const anchor = this.historyNavigation.snapshot.states[String(entry.topicId)] ?? (entry.viewport === null ? null : (0, import_reader_history_model.normalizeReaderHistoryAnchorState)({ viewport: entry.viewport })), opened = await this.openTarget({
	      topicId: entry.topicId,
	      source: "restore"
	    });
	    anchor === null || opened.topic.status !== "opened" && opened.topic.status !== "reused" || await this.historyNavigation.restore(entry.topicId, anchor, {
	      highlight: !1,
	      restoreSemanticState: !1
	    });
	  }
	}
	function createReaderBrowserRuntimeStage(options) {
	  const appearanceByShell = /* @__PURE__ */ new WeakMap(), themeByShell = /* @__PURE__ */ new WeakMap(), fontByShell = /* @__PURE__ */ new WeakMap(), appearancePreferences = options.appearance || void 0, themeOptions = options.theme || void 0, fontPreferences = options.font || void 0, motionPreferences = options.motion || void 0, boostCopyPreferences = options.boostCopy || void 0;
	  if (motionPreferences && options.selectNavigationPreferences)
	    throw new Error(
	      "动画设置与独立导航偏好选择器只能配置一个"
	    );
	  const selectNavigationPreferences = motionPreferences ? (preferences) => (0, import_reader_motion_settings_form.readerMotionNavigationPreferences)(
	    motionPreferences.read(preferences)
	  ) : options.selectNavigationPreferences, shellOptions = appearancePreferences || themeOptions || fontPreferences ? Object.freeze({
	    ...options.shell,
	    createWorkspaceOptions: (shell, context) => {
	      const workspaceOptions = options.shell.createWorkspaceOptions(shell, context), theme = themeOptions ? new import_reader_theme_controller.ReaderThemeController({
	        root: shell.view.root,
	        preferences: themeOptions.preferences,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        system: themeOptions.system,
	        ...themeOptions.clock ? { clock: themeOptions.clock } : {},
	        parentScope: shell.scope
	      }) : null;
	      theme && themeByShell.set(shell, theme);
	      const appearance = appearancePreferences ? new import_reader_appearance_style_controller.ReaderAppearanceStyleController({
	        root: shell.view.root,
	        preferences: appearancePreferences,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        environment: {
	          read: () => Object.freeze({
	            ...workspaceOptions.readAppearance(),
	            ...theme ? { theme: theme.snapshot.resolved } : {}
	          }),
	          subscribe: (listener, scope) => {
	            const publish = () => listener(
	              Object.freeze({
	                ...workspaceOptions.readAppearance(),
	                ...theme ? {
	                  theme: theme.snapshot.resolved
	                } : {}
	              })
	            );
	            return workspaceOptions.appearanceChanges?.subscribe(
	              publish,
	              scope
	            ), theme?.changes.subscribe(publish, scope), () => {
	            };
	          }
	        },
	        parentScope: shell.scope
	      }) : null;
	      appearance && appearanceByShell.set(shell, appearance);
	      const font = fontPreferences ? new import_reader_font_style_controller.ReaderFontStyleController({
	        root: shell.view.root,
	        pageRoot: workspaceOptions.elements.pageRoot,
	        resizeTarget: shell.view.modal,
	        preferences: fontPreferences,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        readReaderWidth: () => shell.view.modal.clientWidth || 1080,
	        readSiteFontFamily: () => {
	          const target = options.runtime.document.body ?? workspaceOptions.elements.pageRoot;
	          return options.runtime.document.defaultView?.getComputedStyle(target).fontFamily || "inherit";
	        },
	        readExternalFontRendering: () => {
	          const pageRoot = workspaceOptions.elements.pageRoot;
	          return pageRoot.hasAttribute("fr-init-once") ? (options.runtime.document.defaultView?.getComputedStyle(pageRoot).getPropertyValue("--fr-render-text").trim() ?? "") !== "" : !1;
	        },
	        userAgent: options.runtime.document.defaultView?.navigator.userAgent ?? "",
	        platform: options.runtime.document.defaultView?.navigator.platform ?? "",
	        ...workspaceOptions.createMutationObserver ? {
	          createMutationObserver: workspaceOptions.createMutationObserver
	        } : {},
	        ...workspaceOptions.createResizeObserver ? {
	          createResizeObserver: workspaceOptions.createResizeObserver
	        } : {},
	        parentScope: shell.scope
	      }) : null;
	      return font && fontByShell.set(shell, font), Object.freeze({
	        ...workspaceOptions,
	        ...appearance ? {
	          readAppearance: () => appearance.snapshot.embedded,
	          appearanceChanges: appearance.embeddedChanges
	        } : {}
	      });
	    }
	  }) : options.shell;
	  return (0, import_reader_workspace_coordinator.createReaderShellWorkspaceStage)({
	    ...shellOptions,
	    onReady(shell, workspace, context) {
	      const theme = themeByShell.get(shell) ?? null, appearance = appearanceByShell.get(shell) ?? null, font = fontByShell.get(shell) ?? null, queryLocalFonts = createReaderLocalFontQuery(
	        options.runtime.document
	      ), rawNavigation = options.runtime.navigation;
	      if (boostCopyPreferences && options.runtime.boostCopy)
	        throw new Error(
	          "Boost 复制偏好投影与自定义 runtime 读取器只能配置一个"
	        );
	      const boostCopy = boostCopyPreferences ? Object.freeze({
	        readSettings: () => boostCopyPreferences.read(
	          context.readPreferences()
	        )
	      }) : options.runtime.boostCopy, topicActionRailPreferences = options.topicActionRail;
	      if (topicActionRailPreferences && options.runtime.topicActionRail !== void 0)
	        throw new Error(
	          "主帖操作列偏好投影与自定义 runtime 端口只能配置一个"
	        );
	      if (topicActionRailPreferences && !context.updatePreferences)
	        throw new Error(
	          "主帖操作列拖动与收纳需要 application 唯一偏好写端口"
	        );
	      const topicActionRail = topicActionRailPreferences === !1 ? !1 : topicActionRailPreferences ? Object.freeze({
	        read: () => topicActionRailPreferences.read(
	          context.readPreferences()
	        ),
	        subscribe: (listener, scope) => context.preferenceChanges.subscribe(
	          (preferences) => listener(
	            topicActionRailPreferences.read(preferences)
	          ),
	          scope
	        ),
	        update: (patch) => {
	          const current = topicActionRailPreferences.read(
	            context.readPreferences()
	          );
	          context.updatePreferences(
	            topicActionRailPreferences.createPatch(
	              Object.freeze({
	                ...current,
	                ...patch
	              })
	            )
	          );
	        }
	      }) : options.runtime.topicActionRail;
	      if (options.selectPerformancePreferences && options.runtime.performance)
	        throw new Error(
	          "性能偏好投影与自定义 runtime performance 只能配置一个"
	        );
	      const performancePolicy = options.selectPerformancePreferences ? new import_reader_performance_policy.ReaderPerformancePolicy({
	        preferences: options.selectPerformancePreferences(
	          context.readPreferences()
	        ),
	        shortBudgetCeiling: options.performanceBudgetCeilings?.short ?? options.runtime.permit.shortBudget,
	        longBudgetCeiling: options.performanceBudgetCeilings?.long ?? options.runtime.permit.longBudget,
	        capabilities: (0, import_reader_performance_policy.readBrowserPerformanceCapabilities)(
	          options.runtime.document.defaultView?.navigator
	        )
	      }) : null;
	      if (selectNavigationPreferences && (rawNavigation?.readOverscan || rawNavigation?.readMaxMountedPostCount || rawNavigation?.readLifetimeMs))
	        throw new Error(
	          "导航偏好投影与自定义虚拟窗口/高亮读取器只能配置一个"
	        );
	      const navigationPreferences = selectNavigationPreferences ? new import_reader_topic_navigation_preferences.ReaderTopicNavigationPreferenceProjection({
	        root: shell.view.root,
	        preferences: selectNavigationPreferences(
	          context.readPreferences()
	        ),
	        ...performancePolicy === null ? {} : {
	          readPerformance: () => performancePolicy.value
	        },
	        parentScope: shell.scope
	      }) : null, navigation = navigationPreferences ? Object.freeze({
	        ...rawNavigation,
	        readOverscan: () => navigationPreferences.readOverscan(),
	        readMaxMountedPostCount: () => navigationPreferences.readMaxMountedPostCount(),
	        readLifetimeMs: () => navigationPreferences.readHighlightLifetimeMs()
	      }) : rawNavigation, rawHistory = options.runtime.history;
	      if (options.selectHistoryNavigationPreferences && rawHistory?.navigationView)
	        throw new Error(
	          "历史导航偏好投影与自定义 navigationView 只能配置一个"
	        );
	      const navigationHistory = options.selectHistoryNavigationPreferences ? Object.freeze({
	        ...rawHistory,
	        navigationView: Object.freeze({
	          preferences: options.selectHistoryNavigationPreferences(
	            context.readPreferences()
	          )
	        })
	      }) : rawHistory;
	      if (options.selectHistoryPanelPreferences && !navigationHistory?.panelView)
	        throw new Error(
	          "历史列表偏好投影需要先配置唯一 panelView 端口"
	        );
	      const history = options.selectHistoryPanelPreferences ? Object.freeze({
	        ...navigationHistory,
	        readSortMode: () => options.selectHistoryPanelPreferences(
	          context.readPreferences()
	        ).sortMode,
	        panelView: Object.freeze({
	          ...navigationHistory.panelView,
	          preferences: options.selectHistoryPanelPreferences(
	            context.readPreferences()
	          )
	        })
	      }) : navigationHistory, rawBookmarks = options.runtime.bookmarks;
	      if (options.selectBookmarkPreferences && rawBookmarks === !1)
	        throw new Error(
	          "收藏偏好投影与禁用收藏中心不能同时配置"
	        );
	      const bookmarks = options.selectBookmarkPreferences ? Object.freeze({
	        ...rawBookmarks ?? {},
	        tabOrder: options.selectBookmarkPreferences(
	          context.readPreferences()
	        ).tabOrder
	      }) : rawBookmarks, rawTimelineView = options.runtime.timelineView;
	      if (options.selectTimelineViewPreferences && (!rawTimelineView || rawTimelineView.readPreferences))
	        throw new Error(
	          "时间轴偏好投影需要唯一 timelineView,且不能同时注入自定义读取器"
	        );
	      let timelinePreferences = options.selectTimelineViewPreferences?.(
	        context.readPreferences()
	      ) ?? null;
	      const timelineView = options.selectTimelineViewPreferences && rawTimelineView ? Object.freeze({
	        ...rawTimelineView,
	        preferences: timelinePreferences,
	        readPreferences: () => timelinePreferences
	      }) : rawTimelineView;
	      if (options.selectTranslationMode === void 0 != (options.persistTranslationMode === void 0))
	        throw new Error(
	          "翻译偏好投影必须同时提供 selectTranslationMode 与 persistTranslationMode"
	        );
	      const rawTranslationView = options.runtime.translationView;
	      if (options.selectTranslationMode && rawTranslationView === !1)
	        throw new Error(
	          "翻译偏好投影与禁用翻译 View 不能同时配置"
	        );
	      if (options.selectTranslationMode && rawTranslationView && (rawTranslationView.initialMode !== void 0 || rawTranslationView.persistMode !== void 0))
	        throw new Error(
	          "翻译偏好投影与自定义 initialMode/persistMode 只能配置一个"
	        );
	      const translationView = options.selectTranslationMode ? Object.freeze({
	        ...rawTranslationView || {},
	        initialMode: options.selectTranslationMode(
	          context.readPreferences()
	        ),
	        persistMode: options.persistTranslationMode
	      }) : rawTranslationView, imagePreferences = options.image || null, rawLightbox = options.runtime.lightbox;
	      if (imagePreferences && rawLightbox?.preferences)
	        throw new Error(
	          "图片偏好投影与自定义 Lightbox 偏好端口只能配置一个"
	        );
	      if (imagePreferences && rawLightbox && !context.updatePreferences)
	        throw new Error(
	          "Lightbox 几何持久化需要 application 唯一偏好写端口"
	        );
	      const lightbox = imagePreferences && rawLightbox ? Object.freeze({
	        ...rawLightbox,
	        preferences: Object.freeze({
	          read: () => {
	            const current = imagePreferences.read(
	              context.readPreferences()
	            );
	            return Object.freeze({
	              originalByDefault: current.lightboxOriginalByDefault,
	              commentsExpanded: current.lightboxCommentsExpandedByDefault,
	              descriptionExpanded: current.lightboxDescriptionExpanded,
	              lightboxDescriptionHeight: current.lightboxDescriptionHeight,
	              lightboxCommentsWidthPercent: current.lightboxCommentsWidthPercent
	            });
	          },
	          update: (patch) => {
	            const current = imagePreferences.read(
	              context.readPreferences()
	            ), next = (0, import_reader_image_preferences.normalizeReaderImagePreferences)({
	              ...current,
	              ...patch.originalByDefault === void 0 ? {} : {
	                lightboxOriginalByDefault: patch.originalByDefault
	              },
	              ...patch.commentsExpanded === void 0 ? {} : {
	                lightboxCommentsExpandedByDefault: patch.commentsExpanded
	              },
	              ...patch.descriptionExpanded === void 0 ? {} : {
	                lightboxDescriptionExpanded: patch.descriptionExpanded
	              },
	              ...patch.lightboxDescriptionHeight === void 0 ? {} : {
	                lightboxDescriptionHeight: patch.lightboxDescriptionHeight
	              },
	              ...patch.lightboxCommentsWidthPercent === void 0 ? {} : {
	                lightboxCommentsWidthPercent: patch.lightboxCommentsWidthPercent
	              }
	            });
	            context.updatePreferences(
	              imagePreferences.createPatch(next)
	            );
	          }
	        })
	      }) : rawLightbox, loadingAnimation = motionPreferences ? new import_reader_loading_animation_view.ReaderLoadingAnimationView({
	        document: options.runtime.document,
	        host: shell.view.body,
	        shell,
	        preference: motionPreferences.read(
	          context.readPreferences()
	        ).loadingAnimation,
	        siteName: motionPreferences.siteName,
	        parentScope: shell.scope
	      }) : null;
	      let downloadCurrentTopic = null;
	      const runtime = new ReaderBrowserRuntime({
	        ...options.runtime,
	        ...performancePolicy === null ? {} : { performance: performancePolicy.value },
	        ...navigation === void 0 ? {} : { navigation },
	        ...history === void 0 ? {} : { history },
	        ...bookmarks === void 0 ? {} : { bookmarks },
	        ...timelineView === void 0 ? {} : { timelineView },
	        ...translationView === void 0 ? {} : { translationView },
	        ...lightbox === void 0 ? {} : { lightbox },
	        ...boostCopy === void 0 ? {} : { boostCopy },
	        ...topicActionRail === void 0 ? {} : { topicActionRail },
	        topicSummaryFonts: Object.freeze({
	          readCurrentFamily: () => options.runtime.document.defaultView?.getComputedStyle(shell.view.root).getPropertyValue("--ldp-post-font-family").trim() || "system-ui,sans-serif",
	          ...queryLocalFonts ? { queryLocalFonts } : {}
	        }),
	        ...options.openQueue && options.runtime.resources ? {
	          downloadCurrentTopic: () => downloadCurrentTopic?.()
	        } : {},
	        shell,
	        workspace,
	        ...loadingAnimation ? { loadingProgress: loadingAnimation } : {},
	        parentScope: shell.scope
	      }), reportCacheError = (cause) => {
	        try {
	          options.runtime.onTopicFeatureError?.(Object.freeze({
	            topicId: runtime.shell.activeTopicId ?? 0,
	            feature: "cache",
	            cause
	          }));
	        } catch {
	        }
	      };
	      new import_browser_request_observation.DiscourseNativeAjaxObservationAdapter({
	        observer: runtime.data.requests,
	        jqueryModule: (0, import_native_host_api.discourseNativeJqueryModule)(options.runtime.host),
	        document: options.runtime.document,
	        hostRequestBudget: runtime.permit
	      }).install(runtime.scope);
	      const refreshTopicButton = shell.view.root.querySelector(
	        ".ldp-reader-refresh"
	      ), closeReaderButton = shell.view.root.querySelector(
	        ".ldp-close"
	      ), layoutToggleButton = shell.view.root.querySelector(
	        ".ldp-layout-toggle"
	      );
	      if (!refreshTopicButton || !closeReaderButton || !layoutToggleButton)
	        throw runtime.destroy(), new Error("Reader Shell 缺少布局、刷新或关闭入口");
	      let currentTopicRefresh = null;
	      const syncHeaderTopicActions = () => {
	        refreshTopicButton.disabled = currentTopicRefresh !== null || runtime.shell.activeValue === null;
	        const presentation = workspace.workspace.snapshot.presentation, fullPage = presentation.fullPage;
	        layoutToggleButton.hidden = presentation.embedded, layoutToggleButton.setAttribute(
	          "aria-pressed",
	          String(fullPage)
	        ), layoutToggleButton.setAttribute(
	          "aria-label",
	          fullPage ? "切换为浮窗阅读器" : "切换为全屏阅读器"
	        ), layoutToggleButton.title = fullPage ? "切换为浮窗阅读器" : "切换为全屏阅读器", layoutToggleButton.replaceChildren((0, import_reader_icon.renderReaderIcon)(
	          options.runtime.document,
	          fullPage ? "minimize-2" : "maximize-2",
	          options.runtime.renderIcon
	        ));
	      }, refreshCurrentTopic = (refreshOptions = {}) => {
	        if (currentTopicRefresh) return currentTopicRefresh;
	        const topicId = runtime.shell.activeTopicId, active = runtime.shell.activeValue;
	        if (!topicId || !active)
	          return Promise.reject(new Error("当前没有可重建的主题"));
	        const transaction = (async () => {
	          const anchor = runtime.historyNavigation.captureCurrent(), exactAnchor = anchor === null ? null : Object.freeze({
	            ...anchor,
	            viewport: Object.freeze({
	              postNumber: anchor.viewport.postNumber,
	              postOffset: anchor.viewport.postOffset,
	              scrollTop: anchor.viewport.scrollTop
	            })
	          }), onlyOpEnabled = active.topicOnlyOp.snapshot.enabled, sources = active.topicImages.snapshot().items.flatMap((item) => [
	            item.previewSrc,
	            item.originalSrc
	          ]);
	          if (runtime.shell.activeTopicId !== topicId || runtime.shell.activeValue !== active)
	            throw new Error("当前主题已切换,已取消缓存重建");
	          if (!await runtime.shell.closeTopic())
	            throw new Error("当前主题关闭事务失败");
	          const cleanupFailures = [];
	          try {
	            const report = await runtime.data.responses.invalidateWithReport({ tags: [`topic:${topicId}`] });
	            cleanupFailures.push(...report.failures.map(
	              (failure) => failure.cause
	            ));
	          } catch (cause) {
	            cleanupFailures.push(cause);
	          }
	          if (refreshOptions.clearImages !== !1 && runtime.imageResources && sources.length)
	            try {
	              const report = await runtime.imageResources.invalidateSources(sources);
	              cleanupFailures.push(...report.failures.map(
	                (failure) => failure.cause
	              ));
	            } catch (cause) {
	              cleanupFailures.push(cause);
	            }
	          const reopened = await runtime.openTarget({
	            topicId,
	            ...exactAnchor ? { postNumber: exactAnchor.viewport.postNumber } : {},
	            source: "restore"
	          });
	          if (reopened.topic.status === "failed") {
	            const refreshCause = reopened.topic.cause, recovered = await (async () => {
	              try {
	                return await active.services.snapshots.persistCurrentSnapshot(), await runtime.openTarget({
	                  topicId,
	                  ...exactAnchor ? { postNumber: exactAnchor.viewport.postNumber } : {},
	                  source: "restore"
	                });
	              } catch (recoveryCause) {
	                throw new AggregateError(
	                  [refreshCause, recoveryCause],
	                  "刷新当前帖子失败,且刷新前内容未能恢复"
	                );
	              }
	            })();
	            if (recovered.topic.status === "failed")
	              throw new AggregateError(
	                [refreshCause, recovered.topic.cause],
	                "刷新当前帖子失败,且刷新前内容未能恢复"
	              );
	            if (recovered.topic.status === "superseded")
	              throw new Error("刷新前内容恢复已被新的打开事务取代");
	            onlyOpEnabled && recovered.topic.value.topicOnlyOp.setEnabled(!0);
	            let recoveryMessage = "刷新当前帖子失败,已恢复刷新前内容;可稍后再试。";
	            if (exactAnchor)
	              try {
	                await runtime.historyNavigation.restore(
	                  topicId,
	                  exactAnchor
	                );
	              } catch {
	                recoveryMessage = "刷新当前帖子失败,已恢复刷新前内容,但之前的阅读位置未能完整还原。";
	              }
	            return Object.freeze({
	              complete: !1,
	              restored: !0,
	              message: recoveryMessage
	            });
	          }
	          if (reopened.topic.status === "superseded")
	            throw new Error("当前主题重建已被新的打开事务取代");
	          return onlyOpEnabled && reopened.topic.value.topicOnlyOp.setEnabled(!0), exactAnchor && await runtime.historyNavigation.restore(topicId, exactAnchor), Object.freeze({
	            complete: cleanupFailures.length === 0,
	            ...cleanupFailures.length ? {
	              message: "当前主题已从原站重新获取,但部分旧缓存未能清理;可再次重试。"
	            } : {}
	          });
	        })();
	        return currentTopicRefresh = transaction, refreshTopicButton.classList.add("is-refreshing"), refreshTopicButton.setAttribute("aria-busy", "true"), syncHeaderTopicActions(), transaction.finally(() => {
	          currentTopicRefresh === transaction && (currentTopicRefresh = null, refreshTopicButton.classList.remove("is-refreshing"), refreshTopicButton.removeAttribute("aria-busy"), syncHeaderTopicActions());
	        }).catch(() => {
	        }), transaction;
	      };
	      runtime.scope.listen(closeReaderButton, "click", () => {
	        runtime.close();
	      }), runtime.scope.listen(refreshTopicButton, "click", (event) => {
	        event.preventDefault(), event.stopPropagation(), !refreshTopicButton.disabled && refreshCurrentTopic().then((result) => {
	          !result.complete && result.message && runtime.feedback.show(result.message);
	        }).catch((cause) => {
	          runtime.feedback.show(
	            cause instanceof Error ? cause.message : "刷新当前帖子失败"
	          );
	        });
	      }), runtime.scope.listen(layoutToggleButton, "click", () => {
	        const mode = workspace.workspace.snapshot.presentation.mode;
	        workspace.setMode(
	          mode === "fullpage" ? "floating" : "fullpage"
	        );
	      }), runtime.shell.changes.subscribe(
	        syncHeaderTopicActions,
	        runtime.scope
	      ), workspace.workspace.changes.subscribe(
	        syncHeaderTopicActions,
	        runtime.scope
	      ), syncHeaderTopicActions();
	      const imageProjection = imagePreferences ? new import_reader_image_preferences.ReaderImagePreferencesProjection({
	        contentRoot: shell.view.root,
	        lightboxRoot: options.runtime.document.documentElement,
	        parentScope: runtime.scope
	      }) : null;
	      if (imageProjection && imagePreferences) {
	        const readImageMode = () => (0, import_reader_image_preferences.readerImagePresentationMode)(
	          workspace.workspace.snapshot
	        ), applyImagePreferences = () => {
	          imageProjection.applyMode(
	            imagePreferences.read(
	              context.readPreferences()
	            ),
	            readImageMode()
	          );
	        };
	        applyImagePreferences(), context.preferenceChanges.subscribe((preferences) => {
	          imageProjection.applyMode(
	            imagePreferences.read(preferences),
	            readImageMode()
	          );
	        }, runtime.scope), workspace.workspace.changes.subscribe(
	          applyImagePreferences,
	          runtime.scope
	        );
	      }
	      appearance?.changes.subscribe(() => {
	        runtime.shell.activeValue?.dom.notifyScroll();
	      }, runtime.scope);
	      const readLayoutMode = () => workspace.workspace.snapshot.presentation.mode === "fullpage" ? "fullpage" : "standard", layout = options.layout ? new import_reader_layout_style_controller.ReaderLayoutStyleController({
	        root: shell.view.root,
	        preferences: options.layout,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        mode: {
	          read: readLayoutMode,
	          subscribe: (listener, scope) => workspace.workspace.changes.subscribe(
	            () => listener(readLayoutMode()),
	            scope
	          )
	        },
	        parentScope: runtime.scope
	      }) : null;
	      if (options.settings !== !1 && options.settings !== void 0 && !context.updatePreferences)
	        throw runtime.destroy(), new Error(
	          "设置 controller 需要 application 唯一偏好写端口"
	        );
	      const settings = options.settings === !1 || !context.updatePreferences ? null : new import_reader_settings_controller.ReaderSettingsController({
	        preferences: {
	          read: context.readPreferences,
	          update: context.updatePreferences
	        },
	        ...options.settings?.initialPanelId === void 0 ? {} : {
	          initialPanelId: options.settings.initialPanelId
	        }
	      });
	      settings && runtime.scope.add(() => settings.destroy());
	      const settingsViewOptions = options.settings ? options.settings.view : void 0, settingsView = settings && settingsViewOptions !== !1 ? new import_reader_settings_view.ReaderSettingsView({
	        document: options.runtime.document,
	        controller: settings,
	        feedback: runtime.feedback,
	        toggleHost: shell.view.root.querySelector(
	          ".ldp-head-btns"
	        ) ?? shell.view.root,
	        surfaceHost: shell.view.surfaceHost,
	        ...options.runtime.renderIcon ? { renderIcon: options.runtime.renderIcon } : {},
	        ...settingsViewOptions?.brandName === void 0 ? {} : { brandName: settingsViewOptions.brandName },
	        ...(settingsViewOptions?.logoUrl ?? shell.view.root.querySelector(
	          "[data-ldp-site-logo]"
	        )?.src) === void 0 ? {} : {
	          logoUrl: settingsViewOptions?.logoUrl ?? shell.view.root.querySelector(
	            "[data-ldp-site-logo]"
	          ).src
	        },
	        parentScope: runtime.scope
	      }) : null;
	      settingsView && theme && context.updatePreferences && new import_reader_theme_settings_control.ReaderThemeSettingsControl({
	        document: options.runtime.document,
	        host: settingsView.themeHost(),
	        theme,
	        persist: context.updatePreferences,
	        ...themeOptions?.hostTheme ? { hostTheme: themeOptions.hostTheme } : {},
	        feedback: runtime.feedback,
	        ...options.runtime.renderIcon ? { renderIcon: options.runtime.renderIcon } : {},
	        parentScope: runtime.scope
	      }), settingsView && new import_reader_window_settings_form.ReaderWindowSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("window"),
	        workspace,
	        parentScope: runtime.scope
	      });
	      const sitesFormOptions = options.settings ? options.settings.sitesForm : void 0;
	      if (sitesFormOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "适用站点 form 需要启用唯一 Settings View"
	        );
	      if (settingsView && sitesFormOptions) {
	        const coordinatedProbe = sitesFormOptions.probe ? new import_browser_discourse_site_probe.CoordinatedDiscourseSiteProbe({
	          gateway: runtime.data.gateway,
	          transport: sitesFormOptions.probe
	        }) : null;
	        new import_reader_custom_site_settings_form.ReaderCustomSiteSettingsForm({
	          document: options.runtime.document,
	          host: settingsView.panelHost("sites"),
	          repository: sitesFormOptions.repository,
	          probe: coordinatedProbe,
	          parentScope: runtime.scope
	        });
	      }
	      const aboutContentOptions = options.settings ? options.settings.aboutContent : void 0;
	      if (aboutContentOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "关于内容需要启用唯一 Settings View"
	        );
	      if (settingsView && aboutContentOptions) {
	        const siteLogo = shell.view.root.querySelector(
	          "[data-ldp-site-logo]"
	        )?.src;
	        new import_reader_about_settings_content.ReaderAboutSettingsContent({
	          document: options.runtime.document,
	          host: settingsView.panelHost("about"),
	          version: aboutContentOptions.version,
	          brandName: (settingsViewOptions ? settingsViewOptions.brandName : void 0) ?? "Awesome LinuxDo Reader",
	          ...siteLogo ? { logoUrl: siteLogo } : {},
	          ...aboutContentOptions.manualUrl ? { manualUrl: aboutContentOptions.manualUrl } : {},
	          parentScope: runtime.scope
	        });
	      }
	      const imageFormOptions = options.settings ? options.settings.imageForm : void 0;
	      if (imageFormOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "图片设置 form 需要启用唯一 Settings View"
	        );
	      if (settingsView && imageFormOptions && new import_reader_image_settings_form.ReaderImageSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("image"),
	        controller: settings,
	        preferences: imageFormOptions,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        parentScope: runtime.scope
	      }), settingsView) {
	        let settingsUserView = null, settingsUsername = "";
	        const mountSettingsUser = () => {
	          const currentUsername = (0, import_native_host_api.discourseNativeCurrentUsername)(
	            options.runtime.host
	          ).toLocaleLowerCase();
	          settingsUserView && currentUsername === settingsUsername || (settingsUserView?.destroy(), settingsUsername = currentUsername, settingsUserView = new import_reader_settings_user_view.ReaderSettingsUserView({
	            document: options.runtime.document,
	            host: settingsView.panelHost("user"),
	            session: runtime.users,
	            username: currentUsername,
	            avatarSource: (template, size) => runtime.userNative.avatarSource(template, size),
	            connectEnabled: !!options.runtime.connect && options.runtime.document.location?.hostname === "linux.do",
	            history: runtime.connectHistory,
	            creditEnabled: options.runtime.document.location?.hostname === "linux.do",
	            ...options.runtime.renderIcon ? { renderIcon: options.runtime.renderIcon } : {},
	            parentScope: runtime.scope,
	            onError: (cause) => {
	              try {
	                options.runtime.onTopicFeatureError?.(Object.freeze({
	                  topicId: runtime.shell.activeTopicId ?? 0,
	                  feature: "user",
	                  cause
	                }));
	              } catch {
	              }
	            }
	          }));
	        };
	        settingsView.changes.subscribe((snapshot) => {
	          snapshot.open && snapshot.activePanelId === "user" && (mountSettingsUser(), settingsUserView?.focusConnect());
	        }, runtime.scope);
	      }
	      const performanceFormOptions = options.settings ? options.settings.performanceForm : void 0;
	      if (performanceFormOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "性能设置 form 需要启用唯一 Settings View"
	        );
	      settingsView && performanceFormOptions && new import_reader_performance_settings_form.ReaderPerformanceSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("performance"),
	        controller: settings,
	        preferences: performanceFormOptions,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        parentScope: runtime.scope
	      });
	      const readingFormOptions = options.settings ? options.settings.readingForm : void 0;
	      if (readingFormOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "阅读与导航设置 form 需要启用唯一 Settings View"
	        );
	      settingsView && readingFormOptions && new import_reader_reading_settings_form.ReaderReadingSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("reading"),
	        controller: settings,
	        preferences: readingFormOptions,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        parentScope: runtime.scope
	      });
	      const translationFormOptions = options.settings ? options.settings.translationForm : void 0;
	      if (translationFormOptions && (!settingsView || !runtime.translationRequests))
	        throw runtime.destroy(), new Error(
	          "翻译与 AI 服务设置 form 需要启用 Settings View 与 TranslationRequestAdapter"
	        );
	      settingsView && translationFormOptions && runtime.translationRequests && (new import_reader_translation_settings_form.ReaderTranslationSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("translation"),
	        repository: translationFormOptions.repository,
	        presentation: translationFormOptions.presentation,
	        parentScope: runtime.scope
	      }), new import_reader_ai_service_settings_form.ReaderAiServiceSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("ai-service"),
	        surfaceHost: shell.view.surfaceHost,
	        repository: translationFormOptions.repository,
	        access: runtime.translationRequests,
	        parentScope: runtime.scope
	      }));
	      const interactionFormOptions = options.settings ? options.settings.interactionForm : void 0;
	      if (interactionFormOptions && !settingsView)
	        throw runtime.destroy(), new Error(
	          "帖子与回复设置 form 需要启用唯一 Settings View"
	        );
	      if (settingsView && interactionFormOptions) {
	        const interactionForm = new import_reader_interaction_settings_form.ReaderInteractionSettingsForm({
	          document: options.runtime.document,
	          host: settingsView.panelHost("interaction"),
	          controller: settings,
	          boostCopy: interactionFormOptions.boostCopy,
	          topicActionRail: interactionFormOptions.topicActionRail,
	          replyTree: interactionFormOptions.replyTree,
	          ...interactionFormOptions.replyTreePreview === void 0 ? {} : {
	            replyTreePreview: interactionFormOptions.replyTreePreview
	          },
	          ...interactionFormOptions.boostsAvailable === void 0 ? {} : {
	            boostsAvailable: interactionFormOptions.boostsAvailable
	          },
	          readPreferences: context.readPreferences,
	          preferenceChanges: context.preferenceChanges,
	          parentScope: runtime.scope
	        });
	        settingsView.changes.subscribe((snapshot) => {
	          snapshot.open && snapshot.activePanelId === "interaction" && interactionForm.refreshCapabilities();
	        }, runtime.scope);
	      }
	      settingsView && layout && new import_reader_layout_settings_form.ReaderLayoutSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("layout"),
	        controller: settings,
	        layout,
	        parentScope: runtime.scope
	      }), settingsView && appearance && new import_reader_appearance_settings_form.ReaderAppearanceSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("appearance"),
	        controller: settings,
	        appearance,
	        parentScope: runtime.scope
	      }), settingsView && font && new import_reader_font_settings_form.ReaderFontSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("font"),
	        controller: settings,
	        font,
	        ...queryLocalFonts ? { queryLocalFonts } : {},
	        parentScope: runtime.scope
	      }), settingsView && motionPreferences && navigationPreferences && new import_reader_motion_settings_form.ReaderMotionSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("flash"),
	        controller: settings,
	        navigation: navigationPreferences,
	        preferences: motionPreferences,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        parentScope: runtime.scope
	      });
	      const webDavOptions = options.settings ? options.settings.webDav : void 0;
	      let webDavCoordinator = null;
	      const configuration = options.settings && options.settings.configuration ? options.settings.configuration : null, configurationManager = configuration ? new import_reader_settings_config_manager.ReaderSettingsConfigManager({
	        codec: new import_reader_settings_config_manager.ReaderSettingsConfigCodec(configuration.codec),
	        defaults: configuration.defaults,
	        preferences: {
	          read: context.readPreferences,
	          update: (preferences) => {
	            context.updatePreferences(preferences);
	          }
	        },
	        customSites: configuration.customSites,
	        translation: configuration.translation,
	        webDav: configuration.webDav
	      }) : null, cacheSurface = settingsView ? new import_reader_cache_management_surface.ReaderCacheManagementSurface({
	        document: options.runtime.document,
	        host: settingsView.panelHost("cache"),
	        ...configurationManager ? {
	          configuration: {
	            export: () => configurationManager.export(),
	            prepare: (payload) => configurationManager.prepare(payload),
	            apply: (prepared) => configurationManager.apply(prepared),
	            reset: async () => {
	              const result = await configurationManager.reset();
	              return (0, import_reader_open_queue_session.requestReaderQueueSurfacePositionsReset)(
	                options.runtime.document
	              ), result;
	            },
	            confirm: (request) => runtime.feedback.confirm(request),
	            saveTextFile: (content, filename) => {
	              if (!runtime.blobDownloads)
	                throw new Error(
	                  "浏览器文件下载 capability 不可用"
	                );
	              runtime.blobDownloads.save(
	                new Blob(
	                  [content],
	                  { type: "application/json" }
	                ),
	                filename
	              );
	            }
	          }
	        } : {},
	        history: runtime.history,
	        chronicle: runtime.chronicle,
	        responses: runtime.data.responses,
	        ...runtime.assetCaches ? { assetCaches: runtime.assetCaches } : {},
	        ...webDavOptions ? {
	          prepareClear: async (categories) => {
	            const {
	              webDavCategories,
	              protectedCategories
	            } = readerWebDavCacheClearPlan(categories);
	            if (!webDavCategories.length)
	              return Object.freeze({
	                failed: Object.freeze([])
	              });
	            try {
	              if (!webDavCoordinator)
	                throw new Error("WebDAV 同步协调器尚未就绪");
	              const release = await webDavCoordinator.acquireLocalCacheClear(webDavCategories);
	              return Object.freeze({
	                failed: Object.freeze([]),
	                release
	              });
	            } catch (cause) {
	              return reportCacheError(cause), Object.freeze({
	                failed: Object.freeze(protectedCategories)
	              });
	            }
	          }
	        } : {},
	        applicationCaches: {
	          stats: async () => {
	            const users = runtime.users.cacheStats(), userObservations = runtime.userObservations.cacheStats(), notifications = runtime.notificationController?.cacheStats() ?? { pages: 0, records: 0 }, bookmarks2 = runtime.bookmarkController?.cacheStats() ?? {
	              bookmarks: 0,
	              reactions: 0,
	              boosts: 0,
	              replies: 0
	            }, creditBridge = await runtime.creditAccount?.cacheStats() ?? {
	              records: 0,
	              bytes: 0,
	              cachedAt: null,
	              expired: !1
	            }, imageObjects = runtime.imageResources?.diagnostics() ?? {
	              objectUrls: 0,
	              objectUrlLimit: 0
	            }, hostIdentity = (0, import_reader_topic_header.readerTopicHostIdentityCacheStats)(
	              options.runtime.document
	            );
	            return Object.freeze({
	              categories: Object.freeze({
	                topics: Object.freeze({
	                  records: (runtime.shell.activeValue ? 1 : 0) + hostIdentity.categoryEntries + hostIdentity.tagEntries,
	                  detail: runtime.shell.activeTopicId === null ? `当前会话:未打开主题;宿主身份派生:${hostIdentity.categoryEntries} 个分类键 · ${hostIdentity.tagEntries} 个标签` : `当前会话:主题 ${runtime.shell.activeTopicId}(清理时联网重建);宿主身份派生:${hostIdentity.categoryEntries} 个分类键 · ${hostIdentity.tagEntries} 个标签`
	                }),
	                users: Object.freeze({
	                  records: users.profiles + users.followLists + users.externalSnapshots + creditBridge.records + userObservations.storedRecords,
	                  detail: `内存热缓存:${users.profiles} 个资料 · ${users.followLists} 份关注列表 · ${users.externalSnapshots} 份账户摘要;用户观察:${userObservations.users} 人 · ${userObservations.memoryRecords} 条内存 / ${userObservations.storedRecords} 条持久投影;LDC bridge ${creditBridge.records} 条(${creditBridge.bytes} B)`
	                }),
	                notifications: Object.freeze({
	                  records: notifications.records,
	                  detail: `内存热缓存:${notifications.pages} 页 · ${notifications.records} 条消息`
	                }),
	                responses: Object.freeze({
	                  records: bookmarks2.bookmarks + bookmarks2.reactions + bookmarks2.boosts + bookmarks2.replies,
	                  detail: `内存热缓存:${bookmarks2.bookmarks} 条收藏 · ${bookmarks2.reactions} 条回应 · ${bookmarks2.boosts} 条 Boost · ${bookmarks2.replies} 条回复`
	                }),
	                assets: Object.freeze({
	                  records: imageObjects.objectUrls,
	                  detail: `内存对象 URL:${imageObjects.objectUrls} / ${imageObjects.objectUrlLimit}`
	                })
	              })
	            });
	          },
	          clear: async (categories) => {
	            const selected = new Set(categories), failed = [];
	            if (selected.has("users"))
	              try {
	                runtime.users.clearCache(), runtime.userObservations.clearCache(), await runtime.creditAccount?.clearCache();
	              } catch (cause) {
	                failed.push("users"), reportCacheError(cause);
	              }
	            if (selected.has("notifications"))
	              try {
	                runtime.notificationController?.clearCache();
	              } catch (cause) {
	                failed.push("notifications"), reportCacheError(cause);
	              }
	            if (selected.has("responses"))
	              try {
	                runtime.bookmarkController?.clearCache();
	              } catch (cause) {
	                failed.push("responses"), reportCacheError(cause);
	              }
	            if (selected.has("topics"))
	              try {
	                runtime.shell.activeValue && ((await refreshCurrentTopic({
	                  clearImages: !1
	                })).complete || failed.push("topics"));
	              } catch (cause) {
	                failed.push("topics"), reportCacheError(cause);
	              } finally {
	                (0, import_reader_topic_header.clearReaderTopicHostIdentityCache)(
	                  options.runtime.document
	                );
	              }
	            return Object.freeze({ failed: Object.freeze(failed) });
	          }
	        },
	        clearImageObjectUrls: () => runtime.imageResources?.clearObjectUrls(),
	        currentTopicAvailable: () => runtime.shell.activeTopicId !== null,
	        clearCurrentTopic: () => refreshCurrentTopic(),
	        notify: (message) => runtime.feedback.show(message),
	        onError: reportCacheError,
	        parentScope: runtime.scope
	      }) : null;
	      if (cacheSurface) {
	        runtime.shell.changes.subscribe(
	          () => cacheSurface.sync(),
	          runtime.scope
	        );
	        let cachePanelVisible = !1;
	        settingsView?.changes.subscribe((snapshot) => {
	          const visible = snapshot.open && snapshot.activePanelId === "cache";
	          visible && !cachePanelVisible && cacheSurface.refresh(), cachePanelVisible = visible;
	        }, runtime.scope);
	      }
	      const resourceMonitor = settingsView ? new import_reader_resource_monitor.ReaderResourceMonitor({
	        document: options.runtime.document,
	        host: settingsView.panelHost("logs"),
	        readerRoot: shell.view.root,
	        requests: runtime.data.requests,
	        schedulerSnapshot: () => runtime.data.client.scheduler.snapshot(),
	        permitSnapshot: () => runtime.permit.snapshot(),
	        performancePolicySnapshot: () => runtime.performance,
	        topicSnapshot: () => {
	          const active = runtime.shell.activeValue, session = active?.services.session, coverage = session?.postStreamCoverage(), unavailableFloors = session?.unavailablePostNumbers() ?? Object.freeze([]), unavailableSet = new Set(
	            unavailableFloors
	          ), relations = active?.replies.topology.snapshot().relations ?? [], views = active?.dom.domOwner.views() ?? [], mountedViews = views.filter(
	            (view) => view.slots.root.isConnected
	          ), roots = mountedViews.map(
	            (view) => view.slots.root
	          ), imageCatalog = active?.topicImages.snapshot(), imageRetry = active?.topicMedia.images.diagnostics(), mediaRuntime = active?.topicMedia.media.diagnostics(), imageResources = runtime.imageResources?.diagnostics(), hlsSources = roots.reduce(
	            (total, root) => total + [
	              ...root.querySelectorAll(
	                "video"
	              )
	            ].filter(
	              (video) => (0, import_reader_media_controller.readerHlsSource)(
	                video,
	                options.runtime.document.baseURI
	              )
	            ).length,
	            0
	          ), nativeHlsSources = roots.reduce(
	            (total, root) => total + [
	              ...root.querySelectorAll(
	                "video"
	              )
	            ].filter((video) => {
	              if (!(0, import_reader_media_controller.readerHlsSource)(
	                video,
	                options.runtime.document.baseURI
	              )) return !1;
	              try {
	                return !!video.canPlayType(
	                  "application/vnd.apple.mpegurl"
	                ) && mediaRuntime?.nativeManagedMediaSource === !0;
	              } catch {
	                return !1;
	              }
	            }).length,
	            0
	          );
	          return Object.freeze({
	            topicId: runtime.shell.activeTopicId,
	            mountedFloors: mountedViews.length,
	            preparedFloors: active?.dom.preparedPostViewCount ?? views.length,
	            retainedFloors: session?.cachedPosts().length ?? 0,
	            nestedFloors: relations.filter(
	              (relation) => relation.parentPostNumber !== null
	            ).length,
	            media: roots.reduce(
	              (total, root) => total + root.querySelectorAll(
	                "img,video,audio,iframe"
	              ).length,
	              0
	            ),
	            initializedFromCache: session?.initializedFromCache ?? !1,
	            expectedFloors: coverage?.expectedPostCount ?? 0,
	            streamFloors: coverage?.streamPostCount ?? 0,
	            missingFloors: coverage?.missingPostCount ?? 0,
	            unavailableFloors,
	            mediaDiagnostics: Object.freeze({
	              catalogImages: imageCatalog?.items.length ?? 0,
	              catalogComplete: imageCatalog?.complete ?? !1,
	              catalogPending: imageCatalog?.pending ?? !1,
	              catalogFailedBatches: imageCatalog?.failedBatchCount ?? 0,
	              persistentCacheEnabled: runtime.imageResources !== null,
	              objectUrls: imageResources?.objectUrls ?? 0,
	              objectUrlLimit: imageResources?.objectUrlLimit ?? 0,
	              boundImages: imageRetry?.boundImages ?? 0,
	              failedImages: imageRetry?.failedImages ?? 0,
	              retryingImages: imageRetry?.retryingImages ?? 0,
	              crossOriginFailures: imageRetry?.crossOriginFailures ?? 0,
	              failedPostNumbers: imageRetry?.failedPostNumbers ?? Object.freeze([]),
	              unavailableSourcePostNumbers: Object.freeze([
	                ...new Set(
	                  (imageCatalog?.items ?? []).map(
	                    (item) => item.sourcePostNumber
	                  ).filter(
	                    (postNumber) => unavailableSet.has(
	                      postNumber
	                    )
	                  )
	                )
	              ].sort((left, right) => left - right)),
	              hlsSources,
	              nativeHlsSources,
	              activeHlsPlayers: mediaRuntime?.activeHlsPlayers ?? 0,
	              hlsLibraryAvailable: mediaRuntime?.hlsLibraryAvailable ?? !1,
	              hlsLibrarySupported: mediaRuntime?.hlsLibrarySupported ?? !1,
	              nativeManagedMediaSource: mediaRuntime?.nativeManagedMediaSource ?? !1
	            })
	          });
	        },
	        performance: options.runtime.document.defaultView?.performance ?? null,
	        parentScope: runtime.scope
	      }) : null;
	      if (settingsView && resourceMonitor) {
	        const syncResourceMonitor = () => {
	          const snapshot = settingsView.snapshot;
	          snapshot.open && snapshot.activePanelId === "logs" ? resourceMonitor.start() : resourceMonitor.stop();
	        };
	        settingsView.changes.subscribe(
	          syncResourceMonitor,
	          runtime.scope
	        ), syncResourceMonitor();
	      }
	      const queuePreferences = options.openQueue || null;
	      if (queuePreferences && !context.updatePreferences)
	        throw runtime.destroy(), new Error(
	          "阅读队列与退出策略需要 application 唯一偏好写端口"
	        );
	      queuePreferences && runtime.composer.installCloseGuard({
	        document: options.runtime.document,
	        enabled: () => queuePreferences.read(
	          context.readPreferences()
	        ).confirmNativeComposerClose,
	        notify: (message) => runtime.feedback.show(message),
	        parentScope: runtime.scope
	      });
	      const queuePrefetchForegroundBusy = () => {
	        if (options.runtime.document.visibilityState !== "visible" || runtime.shell.state === "opening" || runtime.shell.state === "switching") return !0;
	        const lastScrollAt = runtime.shell.activeValue?.dom.lastUserScrollAt() ?? 0, now = options.runtime.document.defaultView?.performance.now() ?? performance.now();
	        return lastScrollAt > 0 && now - lastScrollAt < 700;
	      }, waitForQueuePrefetchIdle = async (signal) => {
	        for (; !signal.aborted && queuePrefetchForegroundBusy(); )
	          await (0, import_coordinated_request_client.abortableDelay)(180, signal);
	        if (signal.aborted) throw signal.reason;
	      }, waitForQueuePrefetchRequestHeadroom = async (signal) => {
	        for (; !signal.aborted; ) {
	          await waitForQueuePrefetchIdle(signal);
	          const snapshot = await runtime.permit.snapshot();
	          if (snapshot.challengeState === "idle" && (0, import_reader_performance_policy.readerQueuePrefetchRequestHasHeadroom)(snapshot)) return;
	          const delayMs = snapshot.nextPermitDelay > 0 ? Math.max(500, Math.min(2e3, snapshot.nextPermitDelay)) : 1e3;
	          await (0, import_coordinated_request_client.abortableDelay)(delayMs, signal);
	        }
	        throw signal.reason;
	      }, waitForTopicDownloadIdle = async (signal) => {
	        for (; !signal.aborted && options.runtime.document.visibilityState === "visible" && queuePrefetchForegroundBusy(); )
	          await (0, import_coordinated_request_client.abortableDelay)(180, signal);
	        if (signal.aborted) throw signal.reason;
	      }, waitForTopicDownloadRequestHeadroom = async (signal, nestedReplies = !1) => {
	        for (; !signal.aborted; ) {
	          await waitForTopicDownloadIdle(signal);
	          const snapshot = await runtime.permit.snapshot();
	          if (snapshot.challengeState === "idle" && (0, import_reader_performance_policy.readerBulkBackgroundRequestHasHeadroom)(snapshot, nestedReplies)) return;
	          const delayMs = snapshot.nextPermitDelay > 0 ? Math.max(180, Math.min(1e3, snapshot.nextPermitDelay)) : 360;
	          await (0, import_coordinated_request_client.abortableDelay)(delayMs, signal);
	        }
	        throw signal.reason;
	      }, topicDownloadRequestMustPause = (error) => runtime.data.client.requestResume(error) !== null, queueTopicPresentation = queuePreferences ? (0, import_native_host_api.discourseNativeTopicPresentation)(options.runtime.host) : null, topicOfflineArtifacts = runtime.blobDownloads ? new import_reader_topic_offline_artifact_repository.ReaderTopicOfflineArtifactRepository(
	        runtime.data.responses,
	        options.runtime.topic.authScope
	      ) : null, topicDownloadQuoteEndpointPreferences = /* @__PURE__ */ new Map(), topicDownloadUnavailableQuoteTargets = /* @__PURE__ */ new Set(), openQueue = queuePreferences ? new import_reader_open_queue_session.ReaderOpenQueueSession({
	        document: options.runtime.document,
	        root: shell.view.modal,
	        workspaceRoot: shell.view.root,
	        storage: options.runtime.storage,
	        authScope: options.runtime.topic.authScope,
	        target: runtime,
	        currentTopicId: () => runtime.shell.activeTopicId,
	        readerOpen: () => [
	          "opening",
	          "switching",
	          "running",
	          "failed"
	        ].includes(runtime.shell.state),
	        historyEntry: (topicId) => topicId ? runtime.history.entry(topicId) : runtime.history.ordered("recent-viewed")[0] ?? null,
	        avatarSource: (template, size) => queueTopicPresentation?.avatarSource(template, size) ?? "",
	        historyAnchor: (topicId) => {
	          const activeAnchor = runtime.historyNavigation.snapshot.states[String(topicId)];
	          if (activeAnchor) return activeAnchor;
	          const viewport = runtime.history.entry(topicId)?.viewport ?? null;
	          return viewport === null ? null : (0, import_reader_history_model.normalizeReaderHistoryAnchorState)({ viewport });
	        },
	        restoreHistoryAnchor: async (topicId, anchor) => {
	          await runtime.historyNavigation.restore(topicId, anchor, {
	            highlight: !1,
	            restoreSemanticState: !1
	          });
	        },
	        prefetch: async (topicId, postNumber, signal, report) => {
	          const scope = runtime.scope.child(), abort = scope.abortController(
	            new DOMException("阅读队列预加载已释放", "AbortError"),
	            signal
	          ), bundle = runtime.data.createTopicBundle({
	            topicId,
	            scope,
	            signal: abort.signal,
	            mount: () => () => {
	            }
	          }, {
	            ...options.runtime.topic,
	            host: options.runtime.host,
	            nativeAjax: runtime.nativeAjax
	          });
	          try {
	            await (0, import_coordinated_request_client.abortableDelay)(900, abort.signal), await waitForQueuePrefetchRequestHeadroom(abort.signal), await bundle.services.session.init({ background: !0 });
	            const session = bundle.services.session, stream = [...session.streamPostIds()], targetPost = postNumber ? session.postByNumber(postNumber) : void 0, targetPostId = Number(
	              targetPost?.id
	            ), foundIndex = targetPostId ? stream.findIndex((postId) => Number(postId) === targetPostId) : -1, targetIndex = foundIndex >= 0 ? foundIndex : Math.max(
	              0,
	              Math.min(stream.length - 1, Number(postNumber ?? 1) - 1)
	            );
	            let ids = stream;
	            if (stream.length > 200 && Number(postNumber ?? 1) > 1) {
	              const before = Math.min(targetIndex, 100);
	              let start = Math.max(0, targetIndex - before), end = Math.min(stream.length, targetIndex + 200 - before);
	              start = Math.max(0, end - 200), end = Math.min(stream.length, start + 200), ids = stream.slice(start, end);
	            } else if (stream.length > 200 && targetIndex > 0) {
	              const before = Math.min(targetIndex, 80);
	              let localStart = Math.max(0, targetIndex - before), localEnd = Math.min(
	                stream.length,
	                localStart + 160
	              );
	              localStart = Math.max(0, localEnd - 160);
	              const selected = new Set(stream.slice(0, 40));
	              stream.slice(localStart, localEnd).forEach((id) => selected.add(id)), selected.size < 200 && stream.slice(localEnd, localEnd + 200 - selected.size).forEach((id) => selected.add(id)), ids = [...selected].slice(0, 200);
	            } else stream.length > 200 && (ids = stream.slice(0, 200));
	            let loadedCount = stream.reduce(
	              (count, postId) => count + +!!session.postById(Number(postId)),
	              0
	            );
	            for (let offset = 0; offset < ids.length; offset += session.pageSize) {
	              const batch = ids.slice(offset, offset + session.pageSize), loadedBefore = batch.reduce(
	                (count, postId) => count + +!!session.postById(Number(postId)),
	                0
	              );
	              await waitForQueuePrefetchRequestHeadroom(abort.signal), await session.loadPostsByIds(
	                batch,
	                { background: !0, maxAttempts: 1 }
	              );
	              const loadedAfter = batch.reduce(
	                (count, postId) => count + +!!session.postById(Number(postId)),
	                0
	              );
	              loadedCount += loadedAfter - loadedBefore, report({
	                loadedCount,
	                totalCount: stream.length
	              });
	            }
	            const nested = await session.loadReplyBranches(
	              ids.map((postId) => Number(
	                session.postById(Number(postId))?.post_number ?? 0
	              )).filter((postNumber2) => Number.isSafeInteger(postNumber2) && postNumber2 > 0),
	              {
	                background: !0,
	                maxPages: 32,
	                maxAttempts: 2,
	                beforePage: () => waitForQueuePrefetchRequestHeadroom(abort.signal)
	              }
	            );
	            report({
	              nestedLoadedCount: nested.loadedReplyCount,
	              nestedTotalCount: nested.expectedReplyCount
	            });
	            const topic = session.topic, mediaPosts = nested.postNumbers.map((childPostNumber) => session.postByNumber(childPostNumber)).filter((post) => post !== void 0), media = runtime.mediaPrefetch ? await runtime.mediaPrefetch.prefetch({
	              posts: mediaPosts,
	              signal: abort.signal,
	              waitUntilIdle: waitForQueuePrefetchRequestHeadroom,
	              reactionSources: (post) => topic ? runtime.postReactions.options(topic, post).flatMap((option) => option.imageUrl ? [option.imageUrl] : []) : [],
	              onProgress: (progress) => report({
	                mediaLoadedCount: progress.loadedCount,
	                mediaTotalCount: progress.totalCount
	              })
	            }) : Object.freeze({
	              loadedCount: 0,
	              totalCount: 0,
	              failedCount: 0,
	              complete: !0
	            });
	            return await session.flush(), loadedCount = stream.reduce(
	              (count, postId) => count + +!!session.postById(Number(postId)),
	              0
	            ), Object.freeze({
	              loadedCount,
	              totalCount: stream.length,
	              nestedLoadedCount: nested.loadedReplyCount,
	              nestedTotalCount: nested.expectedReplyCount,
	              mediaLoadedCount: media.loadedCount,
	              mediaTotalCount: media.totalCount,
	              complete: loadedCount >= stream.length && nested.complete && media.complete
	            });
	          } finally {
	            await bundle.prepareClose?.("close"), scope.destroy();
	          }
	        },
	        ...runtime.blobDownloads ? {
	          topicDownloads: {
	            downloads: runtime.blobDownloads,
	            requestResume: (error) => runtime.data.client.requestResume(error),
	            mount: shell.view.surfaceHost,
	            floating: !0,
	            confirmRemoval: async (context2, host) => {
	              const requestedAt = Number(
	                context2.localDownloadRequestedAt
	              ), localDownload = requestedAt > 0 ? `曾于 ${new Date(requestedAt).toLocaleString(
	                "zh-CN"
	              )} 触发浏览器下载` : "Reader 未记录曾触发本地下载", choice = await runtime.feedback.choose({
	                title: "移除 Topic 下载记录?",
	                message: context2.hasCachedHtml ? "请选择是否同时删除 Reader 缓存中的离线 HTML。" : "该记录没有可清理的 Reader 缓存 HTML。",
	                details: Object.freeze([
	                  Object.freeze({
	                    label: "Reader 缓存 HTML",
	                    value: context2.hasCachedHtml ? "已保存" : "未找到"
	                  }),
	                  Object.freeze({
	                    label: "本地下载",
	                    value: localDownload
	                  }),
	                  ...context2.filename ? [Object.freeze({
	                    label: "文件名",
	                    value: context2.filename
	                  })] : []
	                ]),
	                note: "受浏览器安全限制,Reader 无法检查本地文件是否仍存在,也无法删除下载目录中的文件;如需删除请手动处理。",
	                cancelLabel: "取消",
	                ...context2.hasCachedHtml ? { secondaryLabel: "仅移除记录" } : {},
	                confirmLabel: context2.hasCachedHtml ? "记录和缓存都删除" : "移除记录",
	                icon: "trash"
	              }, host);
	              return choice === "confirm" ? "remove-record-and-cache" : choice === "secondary" ? "remove-record" : "cancel";
	            },
	            confirmBulkRemoval: async (contexts, host) => {
	              const cachedCount = contexts.filter((context2) => context2.hasCachedHtml).length, downloadedCount = contexts.filter((context2) => context2.localDownloadRequestedAt > 0).length, choice = await runtime.feedback.choose({
	                title: `移除 ${contexts.length} 条 Topic 下载记录?`,
	                message: cachedCount ? "请选择是否同时删除这些记录在 Reader 中缓存的离线 HTML。" : "所选记录没有可清理的 Reader 缓存 HTML。",
	                details: Object.freeze([
	                  Object.freeze({
	                    label: "下载记录",
	                    value: `${contexts.length} 条`
	                  }),
	                  Object.freeze({
	                    label: "Reader 缓存 HTML",
	                    value: `${cachedCount} 份`
	                  }),
	                  Object.freeze({
	                    label: "曾触发本地下载",
	                    value: `${downloadedCount} 条`
	                  })
	                ]),
	                note: "Reader 无法检查或删除下载目录中的本地文件;如需删除,请在文件管理器中手动处理。",
	                cancelLabel: "取消",
	                ...cachedCount > 0 ? { secondaryLabel: "仅移除记录" } : {},
	                confirmLabel: cachedCount > 0 ? "记录和缓存都删除" : "移除记录",
	                icon: "trash"
	              }, host);
	              return choice === "confirm" ? "remove-record-and-cache" : choice === "secondary" ? "remove-record" : "cancel";
	            },
	            hydrateHtmlWindow: import_reader_topic_offline_document.hydrateReaderTopicOfflineDocumentWindow,
	            ...topicOfflineArtifacts ? { artifacts: topicOfflineArtifacts } : {},
	            worker: async (topicId, fallbackTitle, signal, report, selection) => {
	              const scope = runtime.scope.child(), abort = scope.abortController(
	                new DOMException("Topic 后台下载已释放", "AbortError"),
	                signal
	              );
	              let backgroundNetworkRequestCount = 0;
	              async function beforeDownloadNetwork(networkSignal, nestedReplies = !1) {
	                await waitForTopicDownloadRequestHeadroom(
	                  networkSignal,
	                  nestedReplies
	                ), backgroundNetworkRequestCount += 1;
	              }
	              const bundle = runtime.data.createTopicBundle({
	                topicId,
	                scope,
	                signal: abort.signal,
	                mount: () => () => {
	                }
	              }, {
	                ...options.runtime.topic,
	                host: options.runtime.host,
	                nativeAjax: runtime.nativeAjax
	              });
	              try {
	                report({
	                  phase: "loading-topic",
	                  detail: "正在读取 Topic 与本地存档"
	                }), await waitForTopicDownloadIdle(abort.signal);
	                const topic = await bundle.services.session.init({
	                  background: !0,
	                  beforeNetwork: beforeDownloadNetwork
	                }), session = bundle.services.session, cachedAtStart = session.cachedPosts(), streamCoverageAtStart = session.postStreamCoverage(), archiveAtStart = session.localArchiveState(), localArchivePlan = (0, import_reader_topic_download_manager.readerTopicDownloadLocalArchivePlan)({
	                  topicStatus: archiveAtStart.topic?.status,
	                  cachedPostCount: cachedAtStart.length,
	                  expectedPostCount: streamCoverageAtStart.expectedPostCount,
	                  streamPostCount: streamCoverageAtStart.streamPostCount,
	                  missingStreamPostCount: streamCoverageAtStart.missingPostCount,
	                  streamComplete: streamCoverageAtStart.complete
	                });
	                let streamComplete = !1, missingCanonicalPostCount = 0, repliesComplete = !1;
	                if (localArchivePlan)
	                  streamComplete = localArchivePlan.streamComplete, missingCanonicalPostCount = localArchivePlan.missingCanonicalPostCount, report({
	                    phase: "loading-replies",
	                    completed: localArchivePlan.completed,
	                    total: localArchivePlan.total,
	                    detail: `正在整理 ${archiveAtStart.topic?.status ?? 404} 本地缓存 ${localArchivePlan.completed}/${localArchivePlan.total}`
	                  });
	                else {
	                  const stream = await session.ensurePostStream({
	                    background: !0,
	                    maxAttempts: 2,
	                    beforeNetwork: beforeDownloadNetwork,
	                    beforeBatch: () => waitForTopicDownloadIdle(abort.signal),
	                    onProgress: (progress) => report({
	                      phase: "loading-posts",
	                      completed: progress.loadedCount,
	                      total: progress.totalCount,
	                      detail: `正文已就绪 ${progress.loadedCount}/${progress.totalCount || "?"} · 本轮复用缓存 ${Math.min(cachedAtStart.length, progress.loadedCount)} · 后台联网 ${backgroundNetworkRequestCount}`
	                    })
	                  });
	                  streamComplete = stream.complete, missingCanonicalPostCount = stream.missingPostIds.length;
	                  const canonicalPosts = session.cachedPosts();
	                  report({
	                    phase: "loading-replies",
	                    completed: canonicalPosts.length,
	                    total: canonicalPosts.length,
	                    detail: `已从 canonical 正文整理回复关系 ${canonicalPosts.length}/${canonicalPosts.length} · 后台联网 ${backgroundNetworkRequestCount}`
	                  }), repliesComplete = stream.complete;
	                }
	                const topicRecord = topic, specialContentWarnings = [];
	                if (topicRecord.is_post_voting === !0 && !localArchivePlan) {
	                  const record = (value) => value !== null && typeof value == "object" && !Array.isArray(value) ? value : null, votingPosts = session.cachedPosts().filter((post) => {
	                    const source = post, loaded = Array.isArray(source.post_voting_comments) ? source.post_voting_comments.length : Array.isArray(source.comments) ? source.comments.length : 0;
	                    return Math.max(0, Number(source.comments_count) || 0) > loaded;
	                  });
	                  for (const [postIndex, post] of votingPosts.entries()) {
	                    const source = post, postId = Number(source.id), postNumber = Number(source.post_number), expectedComments = Math.max(
	                      0,
	                      Number(source.comments_count) || 0
	                    );
	                    if (!Number.isSafeInteger(postId) || postId < 1) continue;
	                    const current = Array.isArray(source.post_voting_comments) ? source.post_voting_comments : Array.isArray(source.comments) ? source.comments : [], merged = /* @__PURE__ */ new Map();
	                    for (const value of current) {
	                      const candidate = record(value), id = Number(candidate?.id);
	                      candidate && Number.isSafeInteger(id) && id > 0 && merged.set(id, Object.freeze({ ...candidate }));
	                    }
	                    try {
	                      let pages = 0;
	                      for (; merged.size < expectedComments && pages < 100; ) {
	                        pages += 1, await waitForTopicDownloadIdle(abort.signal), report({
	                          phase: "loading-replies",
	                          completed: postIndex,
	                          total: votingPosts.length,
	                          detail: `正在补齐楼层 #${postNumber} 的投票评论 · 后台联网 ${backgroundNetworkRequestCount}`
	                        });
	                        const afterCommentId = Math.max(0, ...merged.keys()), payload = await bundle.services.requests.loadPostVotingComments(postId, {
	                          afterCommentId,
	                          refresh: !0,
	                          background: !0,
	                          beforeNetwork: beforeDownloadNetwork
	                        }), payloadRecord = record(payload), incoming = Array.isArray(payload) ? payload : Array.isArray(payloadRecord?.comments) ? payloadRecord.comments : [];
	                        let added = 0;
	                        for (const value of incoming) {
	                          const candidate = record(value), id = Number(candidate?.id);
	                          candidate && Number.isSafeInteger(id) && id > 0 && !merged.has(id) && (added += 1), candidate && Number.isSafeInteger(id) && id > 0 && merged.set(id, Object.freeze({ ...candidate }));
	                        }
	                        if (!added) break;
	                      }
	                      session.ingestPosts([Object.freeze({
	                        ...source,
	                        post_voting_comments: Object.freeze([...merged.values()])
	                      })], "action-response"), merged.size < expectedComments && specialContentWarnings.push(
	                        `楼层 #${postNumber} 投票评论仅保存 ${merged.size}/${expectedComments}`
	                      );
	                    } catch (error) {
	                      if (topicDownloadRequestMustPause(error)) throw error;
	                      specialContentWarnings.push(
	                        `楼层 #${postNumber} 投票评论未能补齐`
	                      );
	                    }
	                  }
	                }
	                const archive = session.localArchiveState(), archived = archive.topic !== null || archive.posts.length > 0, coverage = (0, import_reader_topic_download_manager.readerTopicDownloadCoverage)({
	                  selectionMode: selection.mode,
	                  streamComplete,
	                  missingCanonicalPostCount,
	                  repliesComplete,
	                  archived
	                });
	                await session.flush();
	                const availablePosts = session.cachedPosts(), selected = (0, import_reader_topic_download_manager.selectReaderTopicDownloadPosts)(
	                  availablePosts,
	                  selection,
	                  selection.mode === "op" ? (0, import_reader_topic_header.readerTopicOwnerUsername)(topic, availablePosts) : ""
	                ), contextPosts = selected.posts, contextPostNumbers = new Set(contextPosts.map((post) => Number(post.post_number))), availablePostByNumber = new Map(availablePosts.map((post) => [Number(post.post_number), post])), quotedPosts = /* @__PURE__ */ new Map(), quotePayloadPosts = /* @__PURE__ */ new Map(), quoteTargets = (0, import_reader_topic_offline_document.readerTopicOfflineQuoteTargets)(
	                  options.runtime.document,
	                  Number(topicId),
	                  contextPosts
	                );
	                let missingQuoteTargetCount = 0;
	                for (const [quoteIndex, target] of quoteTargets.entries()) {
	                  if (target.topicId === Number(topicId) && contextPostNumbers.has(target.postNumber)) continue;
	                  const quoteKey = `${target.topicId}:${target.postNumber}`;
	                  if (target.topicId === Number(topicId) && streamComplete && missingCanonicalPostCount === 0 && !availablePostByNumber.has(target.postNumber)) {
	                    missingQuoteTargetCount += 1;
	                    continue;
	                  }
	                  report({
	                    phase: "loading-replies",
	                    completed: quoteIndex,
	                    total: quoteTargets.length,
	                    detail: `正在补齐引用正文 ${quoteIndex + 1}/${quoteTargets.length} · 后台联网 ${backgroundNetworkRequestCount}`
	                  });
	                  let quotedPost = target.topicId === Number(topicId) ? availablePostByNumber.get(target.postNumber) ?? null : quotePayloadPosts.get(quoteKey) ?? null;
	                  if (!quotedPost && !localArchivePlan && !topicDownloadUnavailableQuoteTargets.has(quoteKey)) {
	                    await waitForTopicDownloadIdle(abort.signal);
	                    const targetOptions = {
	                      scope: "single",
	                      background: !0,
	                      beforeNetwork: beforeDownloadNetwork
	                    }, candidates = (0, import_reader_topic_offline_document.prioritizeReaderTopicOfflineTargetCandidates)(
	                      bundle.services.requests.targetCandidates(
	                        target.postNumber,
	                        targetOptions,
	                        target.topicId
	                      ),
	                      topicDownloadQuoteEndpointPreferences.get(target.topicId)
	                    );
	                    for (const candidate of candidates)
	                      try {
	                        const payload = await bundle.services.requests.loadTargetCandidate(
	                          candidate,
	                          target.postNumber,
	                          targetOptions,
	                          target.topicId
	                        );
	                        for (const post of (0, import_topic_session.discoursePostsFromPayload)(payload)) {
	                          const postNumber = Number(post.post_number);
	                          !Number.isSafeInteger(postNumber) || postNumber < 1 || quotePayloadPosts.set(
	                            `${target.topicId}:${postNumber}`,
	                            post
	                          );
	                        }
	                        if (quotedPost = quotePayloadPosts.get(quoteKey) ?? null, quotedPost) {
	                          topicDownloadQuoteEndpointPreferences.set(
	                            target.topicId,
	                            candidate.endpoint
	                          );
	                          break;
	                        }
	                      } catch (error) {
	                        if (error instanceof DOMException && error.name === "AbortError" || topicDownloadRequestMustPause(error)) throw error;
	                        const status = Number(
	                          error?.status
	                        );
	                        if ((0, import_native_request_descriptors.discourseNativeTargetFailureIsDefinitive)({
	                          endpoint: candidate.endpoint,
	                          scope: targetOptions.scope,
	                          status
	                        })) {
	                          topicDownloadUnavailableQuoteTargets.add(quoteKey);
	                          break;
	                        }
	                        if ([401, 403].includes(status)) break;
	                      }
	                  }
	                  quotedPost ? (topicDownloadUnavailableQuoteTargets.delete(quoteKey), quotedPosts.set(
	                    quoteKey,
	                    Object.freeze({ topicId: target.topicId, post: quotedPost })
	                  )) : missingQuoteTargetCount += 1;
	                }
	                missingQuoteTargetCount > 0 && specialContentWarnings.push(
	                  `${missingQuoteTargetCount} 个引用正文未能补齐`
	                );
	                const offlineTranslationController = runtime.translationFeature?.controller ?? null, offlineTranslationMode = offlineTranslationController?.mode ?? "original", offlineTranslationTheme = offlineTranslationController?.theme;
	                let offlineTranslations = null;
	                offlineTranslationController && offlineTranslationMode !== "original" && (offlineTranslations = await offlineTranslationController.prepareOfflineTranslations(
	                  options.runtime.document,
	                  Object.freeze([
	                    ...contextPosts,
	                    ...[...quotedPosts.values()].map((entry) => entry.post)
	                  ]),
	                  abort.signal,
	                  {
	                    onProgress: (completed, total) => report({
	                      phase: "serializing",
	                      completed,
	                      total,
	                      detail: `正在补齐离线译文 ${completed}/${total}`
	                    })
	                  }
	                ));
	                let selectedExpectedPostCount = session.postStreamCoverage().expectedPostCount;
	                const selectedComplete = coverage.complete && specialContentWarnings.length === 0, filenameScope = selected.filenameScope;
	                selection.mode !== "all" && (selectedExpectedPostCount = selected.expectedPostCount), report({
	                  phase: "serializing",
	                  completed: selection.mode === "all" ? contextPosts.length : selected.expectedPostCount,
	                  total: selectedExpectedPostCount,
	                  detail: [
	                    selection.mode === "all" ? "正在生成单文件离线 HTML" : `正在生成离线 HTML · 已准备 ${contextPosts.length} 楼讨论上下文`,
	                    coverage.warning,
	                    ...specialContentWarnings
	                  ].filter(Boolean).join(" · ")
	                });
	                const title = String(
	                  topicRecord.fancy_title ?? topicRecord.title ?? fallbackTitle
	                ).replace(/<[^>]+>/g, "").trim() || fallbackTitle, rootNode = runtime.shell.view.root.getRootNode(), readerRoot = runtime.shell.view.root, readerStyleProperties = {};
	                for (let index = 0; index < readerRoot.style.length; index += 1) {
	                  const name = readerRoot.style.item(index);
	                  name.startsWith("--") && (readerStyleProperties[name] = readerRoot.style.getPropertyValue(name));
	                }
	                const stylesheet = rootNode.querySelector(
	                  "style[data-ldp-reader-shadow]"
	                )?.textContent ?? options.runtime.document.getElementById("ldp-mian-lite-styles")?.textContent ?? "", replyTreePreferences = interactionFormOptions ? interactionFormOptions.replyTree.read(
	                  context.readPreferences()
	                ) : import_reader_reply_tree_preferences.DEFAULT_READER_REPLY_TREE_PREFERENCES, header = (0, import_reader_topic_header.normalizeReaderTopicHeader)(
	                  topic,
	                  contextPosts,
	                  queueTopicPresentation,
	                  topicId
	                ), logo = runtime.shell.view.root.querySelector(".ldp-logo"), offlineKatex = options.runtime.media?.katex ? new import_reader_katex_controller.ReaderKatexController({
	                  document: options.runtime.document,
	                  katex: options.runtime.media.katex
	                }) : null, prepareCooked = (cooked) => {
	                  const host = options.runtime.document.createElement("div");
	                  return host.className = "ldp-content cooked", host.innerHTML = cooked, (0, import_reader_cooked_content_feature2.prepareReaderCookedCallouts)(options.runtime.document, host), offlineKatex?.render(host), offlineTranslations && offlineTranslationController && offlineTranslationController.projectOfflineTranslations(
	                    host,
	                    offlineTranslations
	                  ), host.innerHTML;
	                };
	                let artifact;
	                try {
	                  artifact = (0, import_reader_topic_offline_document.createReaderTopicOfflineDocument)({
	                    topicId,
	                    title,
	                    sourceUrl: new URL(
	                      `/t/${topicId}`,
	                      options.runtime.document.baseURI
	                    ).href,
	                    topic,
	                    posts: contextPosts,
	                    quotedPosts: Object.freeze([...quotedPosts.values()]),
	                    ...selection.mode !== "all" && selected.mainPostNumbers ? {
	                      mainPostNumbers: selected.mainPostNumbers,
	                      projectionMode: selection.mode
	                    } : {},
	                    expectedPostCount: selectedExpectedPostCount,
	                    complete: selectedComplete,
	                    archive,
	                    inlineReplyTreeMaxDepth: replyTreePreferences.inlineReplyTreeMaxDepth,
	                    header,
	                    siteLogoUrl: logo?.currentSrc || logo?.src || "",
	                    reactionEmojiUrl: (reactionId) => (0, import_native_host_api.discourseNativeEmojiUrl)(options.runtime.host, reactionId),
	                    inlineEmojiUrl: (emojiId) => (0, import_native_host_api.discourseNativeEmojiUrl)(options.runtime.host, emojiId),
	                    presentation: Object.freeze({
	                      theme: readerRoot.dataset.ldpTheme === "dark" ? "dark" : "light",
	                      translationMode: offlineTranslationMode,
	                      ...offlineTranslationTheme ? {
	                        translationTheme: offlineTranslationTheme
	                      } : {},
	                      styleProperties: Object.freeze(readerStyleProperties),
	                      structureColorsDisabled: readerRoot.classList.contains(
	                        "ldp-structure-colors-disabled"
	                      )
	                    }),
	                    stylesheet,
	                    prepareCooked
	                  });
	                } finally {
	                  offlineKatex?.destroy();
	                }
	                const downloadArtifact = Object.freeze({
	                  ...artifact,
	                  archiveStatus: archive.topic?.status ?? archive.posts[0]?.status ?? null
	                });
	                return filenameScope ? Object.freeze({
	                  ...downloadArtifact,
	                  filename: downloadArtifact.filename.replace(
	                    /-lite-offline\.html$/,
	                    `-${filenameScope}-lite-offline.html`
	                  )
	                }) : downloadArtifact;
	              } finally {
	                await bundle.prepareClose?.("close"), scope.destroy();
	              }
	            }
	          }
	        } : {},
	        closeReader: () => runtime.close(),
	        composerOpen: () => runtime.composer.isOpen(),
	        readerLightboxOpen: () => !!(0, import_reader_escape_surface.readerSurfaceQuery)(
	          options.runtime.document,
	          ".ldp-lightbox"
	        ),
	        readerSurfaceOpen: () => runtime.readerSurfaceOpen(),
	        closeExpandedReply: () => runtime.closeExpandedReply(),
	        readPreferences: () => queuePreferences.read(
	          context.readPreferences()
	        ),
	        updatePreferences: (patch) => {
	          const current = queuePreferences.read(
	            context.readPreferences()
	          );
	          context.updatePreferences(
	            queuePreferences.createPatch(Object.freeze({
	              ...current,
	              ...patch
	            }))
	          );
	        },
	        notify: (message) => runtime.feedback.show(message),
	        parentScope: runtime.scope
	      }) : null;
	      if (openQueue) {
	        downloadCurrentTopic = () => {
	          openQueue.downloadCurrentTopic();
	        }, runtime.scope.add(() => {
	          downloadCurrentTopic = null;
	        });
	        const syncOpenQueue = () => openQueue.sync();
	        runtime.shell.changes.subscribe(syncOpenQueue, runtime.scope), runtime.history.changes.subscribe(syncOpenQueue, runtime.scope), context.preferenceChanges.subscribe(syncOpenQueue, runtime.scope), workspace.workspace.changes.subscribe(
	          () => openQueue.refreshSurface(),
	          runtime.scope
	        );
	      }
	      const translationOptions = options.settings ? options.settings.translationForm : void 0;
	      if (webDavOptions && !settingsView)
	        throw runtime.destroy(), new Error("WebDAV 设置需要启用唯一 Settings View");
	      if (settingsView && webDavOptions) {
	        const coordinator = new import_reader_webdav_coordinator.ReaderWebDavCoordinator({
	          client: webDavOptions.client,
	          repository: webDavOptions.repository,
	          categories: (0, import_reader_webdav_category_ports.createReaderWebDavCategoryPorts)({
	            history: runtime.history,
	            notifications: runtime.notificationController,
	            bookmarks: runtime.bookmarkController,
	            queue: openQueue,
	            preferences: {
	              read: context.readPreferences,
	              validate: (id, value, records) => {
	                const preferences = context.readPreferences();
	                return (0, import_reader_webdav_category_ports.readerWebDavPreferenceRecordMatchesSchema)(
	                  preferences,
	                  id,
	                  value,
	                  (candidate) => webDavOptions.preferencesCodec.export(candidate).settings,
	                  records
	                );
	              },
	              update: (patch) => {
	                context.updatePreferences(patch);
	              }
	            },
	            topicContext: runtime.threadContextState,
	            customSites: webDavOptions.customSites,
	            connectHistory: runtime.connectHistory,
	            translation: translationOptions ? translationOptions.repository : null,
	            translationCache: options.runtime.translation ? {
	              responses: runtime.data.responses,
	              cache: options.runtime.translation.translationCache
	            } : null,
	            offlineTopics: topicOfflineArtifacts
	          }),
	          hostname: () => options.runtime.document.location.hostname,
	          username: () => (0, import_native_host_api.discourseNativeCurrentUsername)(
	            options.runtime.host
	          )
	        });
	        webDavCoordinator = coordinator, runtime.scope.add(() => {
	          webDavCoordinator === coordinator && (webDavCoordinator = null);
	        });
	        const webDavSettingsForm = new import_reader_webdav_settings_form.ReaderWebDavSettingsForm({
	          document: options.runtime.document,
	          host: settingsView.panelHost("sync"),
	          repository: webDavOptions.repository,
	          coordinator,
	          unavailableReason: () => (0, import_native_host_api.discourseNativeCurrentUsername)(options.runtime.host) ? "" : "当前未登录 Discourse,WebDAV 同步不可用。请先登录并刷新页面。",
	          parentScope: runtime.scope
	        });
	        settingsView.changes.subscribe((snapshot) => {
	          snapshot.open && snapshot.activePanelId === "sync" && webDavSettingsForm.refreshAvailability();
	        }, runtime.scope), new import_reader_webdav_coordinator.ReaderWebDavAutoSync({
	          repository: webDavOptions.repository,
	          coordinator,
	          visibilityState: () => options.runtime.document.visibilityState,
	          parentScope: runtime.scope
	        });
	      }
	      const notificationTrigger = shell.view.root.querySelector(".ldp-notifications-toggle"), historyTrigger = shell.view.root.querySelector(".ldp-history-toggle"), bookmarkTrigger = shell.view.root.querySelector(".ldp-bookmarks-toggle"), settingsTrigger = shell.view.root.querySelector(".ldp-settings-toggle"), exclusivePanels = [
	        ...runtime.notificationController && notificationTrigger ? [{
	          id: "notifications",
	          coexistGroup: "floating-tools",
	          trigger: notificationTrigger,
	          isOpen: () => runtime.notificationController.snapshot.open,
	          open: () => runtime.notificationController.open(),
	          close: () => runtime.notificationController.close()
	        }] : [],
	        ...runtime.historyPanelView && historyTrigger ? [{
	          id: "history",
	          coexistGroup: "floating-tools",
	          trigger: historyTrigger,
	          isOpen: () => runtime.historyPanelView.snapshot.open,
	          open: () => runtime.historyPanelView.open(),
	          close: () => runtime.historyPanelView.close()
	        }] : [],
	        ...runtime.bookmarkController && bookmarkTrigger ? [{
	          id: "bookmarks",
	          coexistGroup: "floating-tools",
	          trigger: bookmarkTrigger,
	          isOpen: () => runtime.bookmarkController.snapshot.open,
	          open: () => runtime.bookmarkController.open(),
	          close: () => runtime.bookmarkController.close()
	        }] : [],
	        ...settingsView && settingsTrigger ? [{
	          id: "settings",
	          trigger: settingsTrigger,
	          isOpen: () => settingsView.snapshot.open,
	          open: () => settingsView.open(),
	          close: () => settingsView.requestClose()
	        }] : []
	      ];
	      exclusivePanels.length > 1 && new import_reader_exclusive_panel_coordinator.ReaderExclusivePanelCoordinator({
	        entries: exclusivePanels,
	        beforeOpen: (target) => {
	          target.id === "settings" && runtime.unwantedTopicView.window.isOpen && runtime.unwantedTopicView.close();
	        },
	        parentScope: runtime.scope,
	        onError: (cause) => {
	          runtime.feedback.show(
	            cause instanceof Error ? cause.message : "面板切换失败"
	          );
	        }
	      });
	      const shortcutPreferences = options.shortcuts || null;
	      if (shortcutPreferences && !context.updatePreferences)
	        throw runtime.destroy(), new Error(
	          "快捷键设置需要 application 唯一偏好写端口"
	        );
	      let fullscreenReturnMode = workspace.workspace.snapshot.presentation.mode === "fullpage" ? "floating" : workspace.workspace.snapshot.presentation.mode;
	      const triggerTopicAction = (selector) => {
	        const button = runtime.shell.activeValue?.topicActionRail?.view?.slots.actions.querySelector(selector);
	        return !button || button.disabled || button.hidden ? !1 : (button.click(), !0);
	      }, triggerHeaderPanel = (selector) => {
	        const trigger = shell.view.root.querySelector(selector);
	        return !trigger || trigger.hidden || trigger.getAttribute("aria-disabled") === "true" || "disabled" in trigger && trigger.disabled ? !1 : (trigger.click(), !0);
	      }, triggerFloatingPanelShortcut = (selector, tabId) => (0, import_reader_floating_window_frame.restoreReaderFloatingWindowTabSession)(
	        shell.view.surfaceHost,
	        tabId
	      ) || triggerHeaderPanel(selector), shortcuts = shortcutPreferences ? new import_reader_shortcut_controller.ReaderShortcutController({
	        target: options.runtime.document,
	        preferences: shortcutPreferences,
	        readPreferences: context.readPreferences,
	        preferenceChanges: context.preferenceChanges,
	        persist: context.updatePreferences,
	        canExecute: (action, event) => readerSurfaceOnlyCloseEvents.has(event) || action === "refreshHost" && !workspace.workspace.snapshot.presentation.embedded || runtime.readerShortcutContextBlocked() ? !1 : action !== "closeReader" ? !0 : (0, import_reader_shortcut_controller.readerShortcutBindingFromEvent)(event) === "Escape" ? !runtime.readerExitBlocked() : !0,
	        onUnavailable: (_action, label) => {
	          runtime.feedback.show(`“${label}”当前不可用`);
	        },
	        execute: (action, event) => {
	          const active = runtime.shell.activeValue;
	          switch (action) {
	            case "historyBack":
	              return runtime.historyNavigation.navigate("back");
	            case "historyForward":
	              return runtime.historyNavigation.navigate("forward");
	            case "topicTop":
	              return active ? active.topicTimeline.jumpTo(1, {
	                alignment: "start",
	                highlight: !0
	              }) : !1;
	            case "topicBottom":
	              return active ? active.topicTimeline.jumpTo(
	                active.topicTimeline.snapshot.totalPostCount,
	                {
	                  alignment: "start",
	                  highlight: !0
	                }
	              ) : !1;
	            case "floorJump":
	              return active?.topicTimelineView ? (active.topicTimelineView.focusJump(), !0) : !1;
	            case "discussionHorizontalScroll":
	              return active?.topicContextSurface.scrollDiscussionHorizontal(
	                event.type === "wheel" ? event.deltaY || event.deltaX : 0
	              ) ?? !1;
	            case "onlyAuthor":
	              return active ? (active.topicOnlyOp.toggle(), !0) : !1;
	            case "translate":
	              return runtime.translationFeature ? (runtime.translationFeature.controller.cycleMode(), !0) : !1;
	            case "refreshTopic":
	              return !active || refreshTopicButton.disabled ? !1 : (refreshTopicButton.click(), !0);
	            case "refreshHost":
	              return (0, import_native_host_api.discourseNativeHostRouteRefresh)(
	                options.runtime.host
	              );
	            case "openOriginal": {
	              const link = shell.view.root.querySelector("a.ldp-open");
	              return !link?.href || link.hidden ? !1 : (link.click(), !0);
	            }
	            case "settings":
	              return triggerHeaderPanel(".ldp-settings-toggle");
	            case "notifications":
	              return triggerFloatingPanelShortcut(
	                ".ldp-notifications-toggle",
	                "notifications"
	              );
	            case "historyPanel":
	              return triggerFloatingPanelShortcut(
	                ".ldp-history-toggle",
	                "history"
	              );
	            case "bookmarksPanel":
	              return triggerFloatingPanelShortcut(
	                ".ldp-bookmarks-toggle",
	                "bookmarks"
	              );
	            case "likeTopic":
	              return triggerTopicAction("button[data-post-like]");
	            case "replyTopic":
	              return triggerTopicAction("button[data-post-reply]");
	            case "bookmarkTopic":
	              return triggerTopicAction("button[data-post-bookmark]");
	            case "toggleFullscreen": {
	              const mode = workspace.workspace.snapshot.presentation.mode;
	              return mode === "fullpage" ? workspace.setMode(fullscreenReturnMode) : (fullscreenReturnMode = mode, workspace.setMode("fullpage"));
	            }
	            case "toggleQueue":
	              return openQueue ? (openQueue.toggle(), !0) : !1;
	            case "closeReader":
	              return runtime.handleCloseReaderShortcut(event);
	          }
	        },
	        onError: (cause) => {
	          runtime.feedback.show(
	            cause instanceof Error ? cause.message : "快捷操作执行失败"
	          );
	        },
	        parentScope: runtime.scope
	      }) : null;
	      settingsView && shortcuts && new import_reader_shortcut_settings_form.ReaderShortcutSettingsForm({
	        document: options.runtime.document,
	        host: settingsView.panelHost("shortcuts"),
	        shortcuts,
	        parentScope: runtime.scope
	      }), (performancePolicy || navigationPreferences || loadingAnimation) && context.preferenceChanges.subscribe((preferences) => {
	        let performanceSnapshot = null;
	        performancePolicy && options.selectPerformancePreferences && (performanceSnapshot = performancePolicy.apply(
	          options.selectPerformancePreferences(preferences)
	        )), navigationPreferences && selectNavigationPreferences && (navigationPreferences.apply(
	          selectNavigationPreferences(preferences)
	        ), navigationPreferences.refreshPerformance()), loadingAnimation && motionPreferences && loadingAnimation.apply(
	          motionPreferences.read(preferences).loadingAnimation
	        ), performanceSnapshot && runtime.applyPerformance(performanceSnapshot);
	      }, runtime.scope), runtime.historyNavigationView && options.selectHistoryNavigationPreferences && context.preferenceChanges.subscribe((preferences) => {
	        runtime.historyNavigationView?.applyPreferences(
	          options.selectHistoryNavigationPreferences(
	            preferences
	          )
	        );
	      }, runtime.scope), runtime.historyPanelView && options.selectHistoryPanelPreferences && context.preferenceChanges.subscribe((preferences) => {
	        const projection = options.selectHistoryPanelPreferences(preferences);
	        runtime.historyPanelView?.applyPreferences(projection), runtime.historyNavigation.refreshOrder();
	      }, runtime.scope), runtime.bookmarkController && options.selectBookmarkPreferences && context.preferenceChanges.subscribe((preferences) => {
	        runtime.bookmarkController?.applyTabOrder(
	          options.selectBookmarkPreferences(preferences).tabOrder
	        );
	      }, runtime.scope), options.selectTimelineViewPreferences && context.preferenceChanges.subscribe((preferences) => {
	        timelinePreferences = options.selectTimelineViewPreferences(preferences), runtime.shell.activeValue?.topicTimelineView?.applyPreferences(timelinePreferences);
	      }, runtime.scope), runtime.translationFeature && options.selectTranslationMode && context.preferenceChanges.subscribe((preferences) => {
	        runtime.translationFeature?.applyMode(
	          options.selectTranslationMode(preferences)
	        ), runtime.translationFeature?.syncMountedPosts();
	      }, runtime.scope);
	      let readyCleanup;
	      try {
	        readyCleanup = options.onReady?.(
	          runtime,
	          context,
	          settings,
	          settingsView,
	          layout,
	          appearance,
	          font
	        ) || void 0;
	      } catch (error) {
	        throw runtime.destroy(), error;
	      }
	      return () => {
	        try {
	          readyCleanup?.();
	        } finally {
	          runtime.destroy();
	        }
	      };
	    }
	  });
	}
}, "dd3101cef603ba71745b16a38678f31504ba6c3cef66d04a01c14171e28914f7");

/* Source: lite/src/app/reader-data-runtime.ts */
runtime.register("src/app/reader-data-runtime.js", function(module, exports, require) {
	var reader_data_runtime_exports = {};
	__export(reader_data_runtime_exports, {
	  READER_CACHE_COORDINATION_CHANNEL: () => READER_CACHE_COORDINATION_CHANNEL,
	  READER_CACHE_COORDINATION_LOCK: () => READER_CACHE_COORDINATION_LOCK,
	  READER_CACHE_COORDINATION_STORAGE_KEY: () => READER_CACHE_COORDINATION_STORAGE_KEY,
	  READER_RESPONSE_CACHE_DATABASE: () => READER_RESPONSE_CACHE_DATABASE,
	  READER_RESPONSE_CACHE_STORE: () => READER_RESPONSE_CACHE_STORE,
	  ReaderDataRuntime: () => ReaderDataRuntime
	});
	module.exports = __toCommonJS(reader_data_runtime_exports);
	var import_cache_coordination = require("../cache/cache-coordination.js"), import_indexeddb_response_cache_store = require("../cache/indexeddb-response-cache-store.js"), import_response_repository = require("../cache/response-repository.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_coordinated_request_client = require("../network/coordinated-request-client.js"), import_domain_request_gateway = require("../network/domain-request-gateway.js"), import_request_rate_limit_policy = require("../network/request-rate-limit-policy.js"), import_request_observer = require("../network/request-observer.js"), import_read_state_coordination = require("../reading/read-state-coordination.js"), import_reader_topic_core_bundle = require("../topic/reader-topic-core-bundle.js");
	const READER_RESPONSE_CACHE_DATABASE = "linuxdo-enhanced-reader:responses:v1", READER_RESPONSE_CACHE_STORE = "responses", READER_CACHE_COORDINATION_STORAGE_KEY = "linuxdo-enhanced-reader:cache-coordination:v1", READER_CACHE_COORDINATION_LOCK = "linuxdo-enhanced-reader:cache-coordination-lock:v1", READER_CACHE_COORDINATION_CHANNEL = "linuxdo-enhanced-reader:cache-coordination-channel:v1";
	function sourceId(value) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error("Reader data runtime sourceId 不能为空");
	  return normalized;
	}
	function browserChannelFactory(value) {
	  return value !== void 0 ? value : typeof BroadcastChannel > "u" ? null : (name) => new BroadcastChannel(name);
	}
	class ReaderDataRuntime {
	  scope;
	  rateLimit;
	  requests;
	  client;
	  responses;
	  gateway;
	  cacheCoordination;
	  readCoordination;
	  #destroyed = !1;
	  constructor(options) {
	    const id = sourceId(options.sourceId), report = (phase, cause) => {
	      options.onDiagnostic?.(Object.freeze({ phase, cause }));
	    };
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    try {
	      const channelFactory = browserChannelFactory(
	        options.broadcastChannelFactory
	      ), cacheChannel = new import_cache_coordination.BroadcastCacheCoordinationChannel({
	        name: READER_CACHE_COORDINATION_CHANNEL,
	        factory: channelFactory,
	        onError: (cause) => report("cache-coordination", cause)
	      }), cacheState = new import_cache_coordination.BrowserCacheCoordinationStatePort({
	        storage: options.storage,
	        storageKey: READER_CACHE_COORDINATION_STORAGE_KEY,
	        lockName: READER_CACHE_COORDINATION_LOCK,
	        locks: options.locks ?? null,
	        onError: (cause) => report("cache-coordination", cause)
	      });
	      this.cacheCoordination = new import_cache_coordination.CrossTabCacheCoordinator({
	        sourceId: id,
	        channel: cacheChannel,
	        state: cacheState,
	        flightTtlMs: options.cacheFlightTtlMs,
	        flightStaleMs: options.cacheFlightStaleMs,
	        ...options.now === void 0 ? {} : { now: options.now },
	        onError: (cause) => report("cache-coordination", cause)
	      }), this.scope.add(() => this.cacheCoordination.close());
	      const store = new import_indexeddb_response_cache_store.IndexedDbResponseCacheStore({
	        databaseName: READER_RESPONSE_CACHE_DATABASE,
	        storeName: READER_RESPONSE_CACHE_STORE,
	        operationTimeoutMs: options.responseOperationTimeoutMs,
	        maxEntries: options.responsePersistentMaxEntries,
	        maxBytes: options.responsePersistentMaxBytes,
	        factory: options.indexedDb ?? null,
	        ...options.now === void 0 ? {} : { now: options.now },
	        onError: (cause) => report("indexeddb", cause)
	      });
	      this.scope.add(() => {
	        store.close();
	      }), this.responses = new import_response_repository.ResponseRepository({
	        store,
	        maxMemoryEntries: options.responseMemoryMaxEntries,
	        maxMemoryBytes: options.responseMemoryMaxBytes,
	        mutationPort: this.cacheCoordination,
	        flightPort: this.cacheCoordination,
	        ...options.cacheFlightHeartbeatMs === void 0 ? {} : { flightHeartbeatMs: options.cacheFlightHeartbeatMs },
	        ...options.cacheFlightWaitTimeoutMs === void 0 ? {} : { flightWaitTimeoutMs: options.cacheFlightWaitTimeoutMs },
	        ...options.now === void 0 ? {} : { now: options.now },
	        onPersistenceError: (cause) => report("indexeddb", cause)
	      }), this.scope.add(this.cacheCoordination.subscribeInvalidation((query) => {
	        this.responses.applyExternalInvalidation(query);
	      }));
	      const readChannel = channelFactory ? new import_read_state_coordination.BroadcastReadStateChannel({
	        createChannel: channelFactory,
	        onListenerError: (cause) => report("read-coordination", cause)
	      }) : void 0, lock = options.locks ? (name, task) => options.locks.request(name, { mode: "exclusive" }, task) : void 0;
	      this.readCoordination = new import_read_state_coordination.BrowserReadStateCoordinator({
	        storage: options.storage,
	        ...readChannel === void 0 ? {} : { channel: readChannel },
	        ...lock === void 0 ? {} : { lock },
	        ...options.now === void 0 ? {} : { now: options.now },
	        ...options.readCoordinationTtlMs === void 0 ? {} : { ttlMs: options.readCoordinationTtlMs },
	        ...options.readCoordinationMaxRecords === void 0 ? {} : { maxRecords: options.readCoordinationMaxRecords },
	        onCoordinationError: (cause) => report("read-coordination", cause)
	      }), this.scope.add(() => this.readCoordination.close()), this.rateLimit = new import_request_rate_limit_policy.RequestRateLimitPolicy(options.rateLimit), this.requests = new import_request_observer.RequestObserver({
	        baseHref: options.rateLimit.baseUrl ?? "https://invalid.local/",
	        retentionMs: 15 * 6e4,
	        maxEntries: 1200,
	        ...options.now === void 0 ? {} : { now: options.now }
	      }), this.client = new import_coordinated_request_client.CoordinatedRequestClient({
	        scheduler: options.scheduler,
	        rateLimitPolicy: this.rateLimit,
	        permitPort: options.permit,
	        observer: this.requests,
	        ...options.now === void 0 ? {} : { now: options.now },
	        ...options.defaultMax429Retries === void 0 ? {} : { defaultMax429Retries: options.defaultMax429Retries },
	        onCoordinationError: (cause) => report("request-coordination", cause)
	      }), this.scope.add(() => this.client.destroy()), this.gateway = new import_domain_request_gateway.DomainRequestGateway(this.client, this.responses);
	    } catch (error) {
	      throw this.scope.destroy(), error;
	    }
	  }
	  createTopicBundle(context, options) {
	    if (this.#destroyed || this.scope.destroyed)
	      throw new Error("ReaderDataRuntime 已销毁");
	    return (0, import_reader_topic_core_bundle.createReaderTopicCoreBundle)(context, {
	      ...options,
	      gateway: this.gateway,
	      responses: this.responses,
	      readCoordination: this.readCoordination
	    });
	  }
	  applyRequestRuntimePolicy(policy) {
	    this.#destroyed || this.scope.destroyed || this.client.applyRuntimePolicy(policy);
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	}
}, "f9a9d054dc7203aa759bf367dfe44a0063643f22727925396089efc5825942b4");

/* Source: lite/src/app/reader-performance-policy.ts */
runtime.register("src/app/reader-performance-policy.js", function(module, exports, require) {
	var reader_performance_policy_exports = {};
	__export(reader_performance_policy_exports, {
	  ReaderPerformancePolicy: () => ReaderPerformancePolicy,
	  readBrowserPerformanceCapabilities: () => readBrowserPerformanceCapabilities,
	  readerBulkBackgroundRequestHasHeadroom: () => readerBulkBackgroundRequestHasHeadroom,
	  readerQueuePrefetchRequestHasHeadroom: () => readerQueuePrefetchRequestHasHeadroom
	});
	module.exports = __toCommonJS(reader_performance_policy_exports);
	const DEFAULTS = Object.freeze({
	  pageSize: 48,
	  streamOverscanScreens: 1.5,
	  streamMaxMountedPostCount: 80,
	  nestedPrefetchScreens: 2.5,
	  requestMaxConcurrent: 3,
	  requestMinIntervalMs: 100,
	  requestRateTargetPercent: 85
	}), BULK_BACKGROUND_REQUEST_BUDGET_SHARE = 0.5, QUEUE_PREFETCH_REQUEST_BUDGET_SHARE = 0.25, QUEUE_PREFETCH_SHORT_REQUEST_LIMIT = 4, QUEUE_PREFETCH_LONG_REQUEST_LIMIT = 8;
	function finiteRange(value, fallback, minimum, maximum) {
	  return Number.isFinite(value) ? Math.min(maximum, Math.max(minimum, value)) : fallback;
	}
	function integerRange(value, fallback, minimum, maximum) {
	  return Math.round(finiteRange(value, fallback, minimum, maximum));
	}
	function positiveInteger(value, name) {
	  const normalized = Number(value);
	  if (!Number.isSafeInteger(normalized) || normalized < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return normalized;
	}
	function readerBulkBackgroundRequestHasHeadroom(input, nestedReplies = !1) {
	  const shortLimit = Math.max(
	    1,
	    Math.min(
	      Math.floor(
	        positiveInteger(input.shortBudget, "shortBudget") * BULK_BACKGROUND_REQUEST_BUDGET_SHARE
	      ),
	      nestedReplies ? 8 : Number.MAX_SAFE_INTEGER
	    )
	  ), longLimit = Math.max(
	    1,
	    Math.min(
	      Math.floor(
	        positiveInteger(input.longBudget, "longBudget") * BULK_BACKGROUND_REQUEST_BUDGET_SHARE
	      ),
	      nestedReplies ? 24 : Number.MAX_SAFE_INTEGER
	    )
	  ), normalizedCount = (value) => Number.isFinite(value) && value >= 0 ? Math.floor(value) : Number.MAX_SAFE_INTEGER;
	  return normalizedCount(input.shortCount) < shortLimit && normalizedCount(input.longCount) < longLimit;
	}
	function readerQueuePrefetchRequestHasHeadroom(input) {
	  const shortLimit = Math.max(
	    1,
	    Math.min(
	      Math.floor(
	        positiveInteger(input.shortBudget, "shortBudget") * QUEUE_PREFETCH_REQUEST_BUDGET_SHARE
	      ),
	      QUEUE_PREFETCH_SHORT_REQUEST_LIMIT
	    )
	  ), longLimit = Math.max(
	    1,
	    Math.min(
	      Math.floor(
	        positiveInteger(input.longBudget, "longBudget") * QUEUE_PREFETCH_REQUEST_BUDGET_SHARE
	      ),
	      QUEUE_PREFETCH_LONG_REQUEST_LIMIT
	    )
	  ), normalizedCount = (value) => Number.isFinite(value) && value >= 0 ? Math.floor(value) : Number.MAX_SAFE_INTEGER;
	  return normalizedCount(input.shortCount) < shortLimit && normalizedCount(input.longCount) < longLimit;
	}
	function positiveFinite(value) {
	  const normalized = Number(value);
	  return Number.isFinite(normalized) && normalized > 0 ? normalized : void 0;
	}
	function readBrowserPerformanceCapabilities(navigatorValue) {
	  const source = navigatorValue, logicalProcessors = positiveFinite(source?.hardwareConcurrency), memoryGiB = positiveFinite(source?.deviceMemory), effectiveType = String(
	    source?.connection?.effectiveType ?? ""
	  ).trim().toLowerCase();
	  return Object.freeze({
	    ...logicalProcessors === void 0 ? {} : { logicalProcessors },
	    ...memoryGiB === void 0 ? {} : { memoryGiB },
	    ...source?.connection?.saveData === !0 ? { saveData: !0 } : {},
	    ...effectiveType ? { effectiveType } : {}
	  });
	}
	function runtimeScales(capabilities) {
	  const cores = positiveFinite(capabilities.logicalProcessors), memory = positiveFinite(capabilities.memoryGiB), coreScale = cores === void 0 ? 1 : cores <= 2 ? 0.55 : cores <= 4 ? 0.75 : cores <= 6 ? 0.9 : 1, memoryScale = memory === void 0 ? 1 : memory <= 2 ? 0.55 : memory <= 4 ? 0.75 : memory <= 6 ? 0.9 : 1, connectionScale = capabilities.saveData === !0 ? 0.55 : capabilities.effectiveType === "slow-2g" ? 0.45 : capabilities.effectiveType === "2g" ? 0.6 : capabilities.effectiveType === "3g" ? 0.8 : 1;
	  return Object.freeze({
	    render: Math.min(coreScale, memoryScale),
	    network: connectionScale
	  });
	}
	function snapshot(preferences, shortBudgetCeiling, longBudgetCeiling, capabilities) {
	  const scales = runtimeScales(capabilities), batchScale = Math.min(scales.render, scales.network), pageSize = integerRange(
	    preferences.performancePageSize,
	    DEFAULTS.pageSize,
	    12,
	    64
	  ), streamMaxMountedPostCount = integerRange(
	    preferences.performanceStreamMaxItems,
	    DEFAULTS.streamMaxMountedPostCount,
	    24,
	    128
	  ), nestedPrefetchScreens = finiteRange(
	    preferences.performanceNestedPrefetch,
	    DEFAULTS.nestedPrefetchScreens,
	    1,
	    3
	  ), requestMaxConcurrent = integerRange(
	    preferences.performanceRequestConcurrency,
	    DEFAULTS.requestMaxConcurrent,
	    1,
	    4
	  ), requestMinIntervalMs = integerRange(
	    preferences.performanceRequestInterval,
	    DEFAULTS.requestMinIntervalMs,
	    80,
	    500
	  ), requestRateTargetPercent = integerRange(
	    preferences.performanceRequestRateTarget,
	    DEFAULTS.requestRateTargetPercent,
	    50,
	    95
	  ), target = requestRateTargetPercent / 100;
	  return Object.freeze({
	    pageSize: integerRange(
	      Math.floor(pageSize * batchScale),
	      pageSize,
	      12,
	      64
	    ),
	    streamOverscanScreens: finiteRange(
	      preferences.performanceStreamOverscan,
	      DEFAULTS.streamOverscanScreens,
	      0.25,
	      3
	    ),
	    streamMaxMountedPostCount: integerRange(
	      Math.floor(streamMaxMountedPostCount * scales.render),
	      streamMaxMountedPostCount,
	      24,
	      128
	    ),
	    nestedPrefetchScreens: finiteRange(
	      nestedPrefetchScreens * scales.network,
	      nestedPrefetchScreens,
	      1,
	      3
	    ),
	    requestMaxConcurrent: integerRange(
	      Math.round(requestMaxConcurrent * batchScale),
	      requestMaxConcurrent,
	      1,
	      4
	    ),
	    requestMinIntervalMs: integerRange(
	      Math.ceil(requestMinIntervalMs / Math.max(0.35, batchScale)),
	      requestMinIntervalMs,
	      80,
	      500
	    ),
	    requestRateTargetPercent,
	    requestShortBudget: Math.max(
	      1,
	      Math.floor(shortBudgetCeiling * target)
	    ),
	    requestLongBudget: Math.max(
	      1,
	      Math.floor(longBudgetCeiling * target)
	    )
	  });
	}
	class ReaderPerformancePolicy {
	  #shortBudgetCeiling;
	  #longBudgetCeiling;
	  #capabilities;
	  #snapshot;
	  constructor(options) {
	    this.#shortBudgetCeiling = positiveInteger(
	      options.shortBudgetCeiling,
	      "shortBudgetCeiling"
	    ), this.#longBudgetCeiling = positiveInteger(
	      options.longBudgetCeiling,
	      "longBudgetCeiling"
	    ), this.#capabilities = Object.freeze({
	      ...options.capabilities ?? {}
	    }), this.#snapshot = snapshot(
	      options.preferences,
	      this.#shortBudgetCeiling,
	      this.#longBudgetCeiling,
	      this.#capabilities
	    );
	  }
	  get value() {
	    return this.#snapshot;
	  }
	  apply(preferences) {
	    const next = snapshot(
	      preferences,
	      this.#shortBudgetCeiling,
	      this.#longBudgetCeiling,
	      this.#capabilities
	    );
	    return Object.entries(next).every(
	      ([key, value]) => this.#snapshot[key] === value
	    ) ? this.#snapshot : (this.#snapshot = next, next);
	  }
	}
}, "f4736053ffeb43148dd948d0c45db5e91fc3b35141e277b994abe0bf2b64f814");

/* Source: lite/src/components/reader-control-tooltip.ts */
runtime.register("src/components/reader-control-tooltip.js", function(module, exports, require) {
	var reader_control_tooltip_exports = {};
	__export(reader_control_tooltip_exports, {
	  ReaderControlTooltip: () => ReaderControlTooltip
	});
	module.exports = __toCommonJS(reader_control_tooltip_exports);
	var import_event_target = require("../dom/event-target.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js");
	const TOOLTIP_CONTROL_SELECTOR = [
	  "button",
	  "a",
	  '[role="button"]',
	  "[data-ldp-tooltip-label]",
	  ".ldp-nested-branch-toggle",
	  ".ldp-avatar-flair",
	  ".ldp-user-card-badge"
	].join(","), READER_SURFACE_SELECTOR = [
	  ".ldp-overlay",
	  ".ldp-lightbox",
	  ".ldp-user-card-fallback",
	  ".ldp-avatar-viewer"
	].join(","), HOST_TOPIC_CARD_SELECTOR = ":is(.topic-list-item,.latest-topic-list-item)";
	function domNode(value) {
	  return value !== null && typeof value == "object" && typeof value.nodeType == "number";
	}
	class ReaderControlTooltip {
	  scope;
	  element;
	  #document;
	  #copyText;
	  #schedule;
	  #cancelSchedule;
	  #activeControl = null;
	  #copyResetTimer = 0;
	  constructor(options) {
	    this.#document = options.document, this.#copyText = options.copyText ?? null;
	    const viewport = this.#document.defaultView;
	    this.#schedule = options.schedule ?? ((callback, delayMs) => viewport ? viewport.setTimeout(callback, delayMs) : globalThis.setTimeout(callback, delayMs)), this.#cancelSchedule = options.cancelSchedule ?? ((handle) => {
	      viewport ? viewport.clearTimeout(handle) : globalThis.clearTimeout(handle);
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.element = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-reader-icon-tooltip ldp-transient-surface"
	    ), this.element.role = "tooltip", this.element.hidden = !0, options.surfaceHost.append(this.element);
	    const interactionRoot = options.surfaceHost.getRootNode(), roots = [interactionRoot];
	    interactionRoot !== this.#document && roots.push(this.#document);
	    for (const root of roots) this.#listen(root);
	    viewport && this.scope.listen(viewport, "resize", () => this.close());
	    for (const type of [
	      "ldp-reader-window-change",
	      "ldp-reader-workspace-change"
	    ])
	      this.scope.listen(options.surfaceHost, type, () => this.close());
	    this.scope.add(() => {
	      this.#clearCopyReset(), this.close(), this.element.remove();
	    });
	  }
	  refresh(control) {
	    this.#activeControl && !this.#activeControl.isConnected && this.close();
	    const match = this.#match(control);
	    if (!match || control.hidden || !this.#keepOpen(control)) {
	      control === this.#activeControl && this.close();
	      return;
	    }
	    this.#show(match);
	  }
	  close() {
	    !this.#activeControl && this.element.hidden && !this.element.textContent || (this.#activeControl = null, this.element.hidden = !0, this.element.textContent = "", this.element.classList.remove(
	      "ldp-reader-history-tooltip",
	      "ldp-connect-help-tooltip"
	    ));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #listen(root) {
	    this.scope.listen(root, "ldp-tooltip-refresh", (event) => {
	      const control = (0, import_event_target.eventElement)(event);
	      control && this.refresh(control);
	    }), this.scope.listen(root, "click", (event) => {
	      this.#copyNamedTarget(event);
	    }, !0), this.scope.listen(root, "pointerover", (event) => {
	      const pointer = event, match = this.#match((0, import_event_target.eventElement)(event));
	      !match || domNode(pointer.relatedTarget) && match.control.contains(pointer.relatedTarget) || this.#show(match, pointer);
	    }), this.scope.listen(root, "pointerdown", (event) => {
	      (0, import_event_target.eventElement)(event)?.closest(
	        ".ldp-header[data-ldp-reader-drag-surface]"
	      ) && this.close();
	    }, !0), this.scope.listen(root, "pointermove", (event) => {
	      const active = this.#activeControl;
	      !active?.matches(
	        ".ldp-nested-rail-toggle,.ldp-nested-branch-toggle"
	      ) || !domNode(event.target) || !active.contains(event.target) || this.#position(active, event);
	    }), this.scope.listen(root, "pointerout", (event) => {
	      const pointer = event, active = this.#activeControl;
	      !active || domNode(pointer.relatedTarget) && active.contains(pointer.relatedTarget) || active.matches(":focus-visible") || this.close();
	    }), this.scope.listen(root, "focusin", (event) => {
	      const match = this.#match((0, import_event_target.eventElement)(event));
	      match && this.#show(match);
	    }), this.scope.listen(root, "focusout", () => {
	      queueMicrotask(() => {
	        this.#keepOpen(this.#activeControl) || this.close();
	      });
	    }), this.scope.listen(root, "scroll", () => {
	      const active = this.#activeControl;
	      if (!active) return;
	      if (!active.matches(".ldp-reader-history-nav")) {
	        this.close();
	        return;
	      }
	      const hovered = this.#queryHoveredHistoryControl(root), match = this.#match(hovered);
	      match ? this.#show(match) : this.close();
	    }, !0);
	  }
	  #queryHoveredHistoryControl(root) {
	    if (!("querySelector" in root)) return null;
	    const query = root.querySelector;
	    return typeof query == "function" ? query.call(root, ".ldp-reader-history-nav:hover") : null;
	  }
	  #match(target) {
	    const control = target?.closest(TOOLTIP_CONTROL_SELECTOR) ?? null;
	    if (!control || control.hasAttribute("data-ldp-native-dnd")) return null;
	    const namedHostTopicControl = control.hasAttribute("data-ldp-tooltip-label") && this.#document.documentElement.classList.contains(
	      "ldp-reader-workspace"
	    ) && !!control.closest(HOST_TOPIC_CARD_SELECTOR);
	    if (!!!(control.closest(READER_SURFACE_SELECTOR) || control.matches(".ldp-native-reader-trigger") || namedHostTopicControl) || control.hasAttribute("data-tooltip") || control.matches(
	      '.ldp-settings-tab,.ldp-topic-timeline-track,[data-reaction-picker][aria-expanded="true"]'
	    )) return null;
	    const functional = control.matches(
	      'button,[role="button"],.ldp-nested-branch-toggle'
	    ), iconOnlyLink = control.matches("a") && !!control.querySelector(".ldp-icon,.ldp-logo,img") && !this.#hasVisibleText(control), namedCopyTarget = control.matches(
	      ".ldp-avatar-flair,.ldp-user-card-badge"
	    ), namedTarget = control.hasAttribute("data-ldp-tooltip-label");
	    if (!functional && !iconOnlyLink && !namedCopyTarget && !namedTarget)
	      return null;
	    const narrowTitle = control.matches(".ldp-title-jump") && (control.closest(".ldp-modal")?.getBoundingClientRect().width ?? 0) <= 480 && control.scrollWidth > control.clientWidth + 1, label = String(
	      narrowTitle ? control.textContent : control.getAttribute("aria-label") ?? control.dataset.ldpTooltipLabel ?? ""
	    ).trim();
	    return label ? Object.freeze({ control, label }) : null;
	  }
	  #hasVisibleText(control) {
	    const viewport = this.#document.defaultView;
	    return [...control.childNodes].some((node) => {
	      if (node.nodeType === 3) return !!node.textContent?.trim();
	      if (node.nodeType !== 1) return !1;
	      const child = node;
	      if (child.matches(".ldp-icon,.ldp-logo,.ldp-notification-unread-badge"))
	        return !1;
	      if (viewport?.getComputedStyle) {
	        const computed = viewport.getComputedStyle(child);
	        if (computed.display === "none" || computed.visibility === "hidden")
	          return !1;
	      }
	      return !!(child.innerText?.trim() || child.textContent?.trim());
	    });
	  }
	  #show(match, pointer = null) {
	    match.control.removeAttribute("title"), this.#activeControl = match.control, this.element.textContent = match.label, this.element.classList.toggle(
	      "ldp-reader-history-tooltip",
	      match.control.matches(".ldp-reader-history-nav")
	    ), this.element.classList.toggle(
	      "ldp-connect-help-tooltip",
	      match.control.matches(".ldp-connect-metric")
	    ), this.element.hidden = !1, this.#position(match.control, pointer);
	  }
	  #position(control, pointer) {
	    const viewport = this.#document.defaultView;
	    if (!viewport) return;
	    const rect = control.getBoundingClientRect(), tooltipRect = this.element.getBoundingClientRect(), edge = 8;
	    if (control.matches(".ldp-nested-rail-toggle,.ldp-nested-branch-toggle") && pointer && Number.isFinite(pointer.clientX) && Number.isFinite(pointer.clientY)) {
	      let left2 = pointer.clientX + 12;
	      left2 + tooltipRect.width > viewport.innerWidth - edge && (left2 = pointer.clientX - tooltipRect.width - 12);
	      let top2 = pointer.clientY + 12;
	      top2 + tooltipRect.height > viewport.innerHeight - edge && (top2 = pointer.clientY - tooltipRect.height - 12), this.#place(left2, top2, tooltipRect, edge);
	      return;
	    }
	    let left = rect.left + (rect.width - tooltipRect.width) / 2;
	    left = Math.max(
	      edge,
	      Math.min(left, viewport.innerWidth - tooltipRect.width - edge)
	    );
	    let top = rect.top - tooltipRect.height - 6;
	    top < edge && (top = Math.min(
	      viewport.innerHeight - tooltipRect.height - edge,
	      rect.bottom + 6
	    )), this.#place(left, top, tooltipRect, edge);
	  }
	  #place(left, top, rect, edge) {
	    const viewport = this.#document.defaultView;
	    this.element.style.left = `${Math.round(Math.max(
	      edge,
	      Math.min(left, viewport.innerWidth - rect.width - edge)
	    ))}px`, this.element.style.top = `${Math.round(Math.max(
	      edge,
	      Math.min(top, viewport.innerHeight - rect.height - edge)
	    ))}px`;
	  }
	  #keepOpen(control) {
	    if (!control) return !1;
	    try {
	      return control.matches(":hover") || control.matches(":focus-visible");
	    } catch {
	      return !1;
	    }
	  }
	  #copyNamedTarget(event) {
	    if (!this.#copyText) return;
	    const target = (0, import_event_target.eventElement)(event)?.closest(
	      ".ldp-avatar-flair,.ldp-user-card-badge"
	    ) ?? null;
	    if (!target || !target.closest(READER_SURFACE_SELECTOR)) return;
	    const original = String(
	      target.dataset.ldpTooltipLabel ?? target.getAttribute("aria-label") ?? ""
	    ).trim();
	    original && (event.preventDefault(), event.stopPropagation(), Promise.resolve(this.#copyText(original)).then(() => this.#showCopyState(target, original, "已复制")).catch(() => this.#showCopyState(target, original, "复制失败")));
	  }
	  #showCopyState(target, original, message) {
	    this.#clearCopyReset(), target.dataset.ldpTooltipLabel = message, target.setAttribute("aria-label", message), this.refresh(target), this.#copyResetTimer = this.#schedule(() => {
	      this.#copyResetTimer = 0, !(!target.isConnected || target.dataset.ldpTooltipLabel !== message) && (target.dataset.ldpTooltipLabel = original, target.setAttribute("aria-label", original), this.refresh(target));
	    }, 900);
	  }
	  #clearCopyReset() {
	    this.#copyResetTimer && (this.#cancelSchedule(this.#copyResetTimer), this.#copyResetTimer = 0);
	  }
	}
}, "8c6538bb3b9ca78569693905b692efac4491c47d8b3a091d5629adde434613bb");

/* Source: lite/src/components/reader-icon.ts */
runtime.register("src/components/reader-icon.js", function(module, exports, require) {
	var reader_icon_exports = {};
	__export(reader_icon_exports, {
	  createReaderIcon: () => createReaderIcon,
	  hasReaderIcon: () => hasReaderIcon,
	  readerIconSvgMarkup: () => readerIconSvgMarkup,
	  renderReaderIcon: () => renderReaderIcon,
	  resolveReaderIcon: () => resolveReaderIcon
	});
	module.exports = __toCommonJS(reader_icon_exports);
	const SVG_NAMESPACE = "http://www.w3.org/2000/svg", ICON_PATHS = Object.freeze({
	  activity: "M3 12h4l2-7 4 14 2-7h6",
	  "alert-triangle": "M10.3 2.9 1.8 17a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 2.9a2 2 0 0 0-3.4 0ZM12 9v4m0 4h.01",
	  "arrow-up": "M12 19V5m-7 7 7-7 7 7",
	  award: "M18 8a6 6 0 1 1-12 0 6 6 0 0 1 12 0Zm-2.5 5L17 22l-5-3-5 3 1.5-9",
	  bookmark: "m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z",
	  "book-open": "M2 4h6a4 4 0 0 1 4 4v12a4 4 0 0 0-4-4H2ZM22 4h-6a4 4 0 0 0-4 4v12a4 4 0 0 1 4-4h6Z",
	  bell: "M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4",
	  "bell-off": "M13.7 21h-3.4M18 8a6 6 0 0 0-9.3-5M6.3 6.3A6 6 0 0 0 6 8c0 7-3 7-3 9h14M3 3l18 18",
	  check: "m5 12 4 4L19 6",
	  "chevron-down": "m6 9 6 6 6-6",
	  "chevron-right": "m9 18 6-6-6-6",
	  "chevron-up": "m18 15-6-6-6 6",
	  "circle-x": "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM15 9l-6 6m0-6 6 6",
	  "circle-help": "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM9.1 9a3 3 0 1 1 5.4 1.8c-.8 1-2.5 1.4-2.5 3.2m0 4h.01",
	  clock: "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM12 6v6l4 2",
	  "clock-check": "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0Zm-14 0 3 3 5-6",
	  "check-square": "M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
	  "chevron-left": "m15 18-6-6 6-6",
	  copy: "M9 9h11v11H9zM4 15H3V4h11v1",
	  code: "m16 18 6-6-6-6M8 6l-6 6 6 6",
	  download: "M12 3v12m-5-5 5 5 5-5M5 21h14",
	  droplet: "M12 2.69 5.66 9a9 9 0 1 0 12.68 0Z",
	  "external-link": "M15 3h6v6m0-6-9 9M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",
	  "eye-off": "M3 3l18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.9 4.2A10.5 10.5 0 0 1 21 12a12 12 0 0 1-2.1 3M6.6 6.6A12 12 0 0 0 3 12a10.5 10.5 0 0 0 9 5.2",
	  info: "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM12 11v6m0-10h.01",
	  heart: "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z",
	  hand: "M18 11V6a2 2 0 0 0-4 0v5M14 10V4a2 2 0 0 0-4 0v6M10 9.5V6a2 2 0 0 0-4 0v8M6 14v-2a2 2 0 0 0-4 0v2a8 8 0 0 0 8 8h2c5.5 0 10-4.5 10-10V8a2 2 0 0 0-4 0v3",
	  link: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
	  languages: "M5 8l6 6M4 14l6-6 2-3M2 5h12M7 2h1m14 20-5-10-5 10M14 18h6",
	  layers: "m12 2 9 5-9 5-9-5 9-5Zm-9 10 9 5 9-5M3 17l9 5 9-5",
	  lightbulb: "M9 18h6m-5 4h4m4-10a6 6 0 1 0-10 5c.7.5 1 1.3 1 2h6c0-1 .3-1.5 1-2a6 6 0 0 0 2-5Z",
	  list: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01",
	  "list-checks": "m3 6 2 2 4-4M3 12l2 2 4-4M3 18l2 2 4-4M13 6h8M13 12h8M13 18h8",
	  loader: "M21 12a9 9 0 1 1-6.219-8.56",
	  "maximize-2": "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7",
	  "minimize-2": "M4 14h6v6M10 14l-7 7M20 10h-6V4m0 6 7-7",
	  minus: "M5 12h14",
	  maximize: "M8 3H3v5m18 0V3h-5M3 16v5h5m8 0h5v-5",
	  "message-square": "M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z",
	  pencil: "m12 20 9-9-4-4-9 9-1 5 5-1ZM15 9l4 4",
	  plus: "M12 5v14M5 12h14",
	  "rotate-ccw": "M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5",
	  reply: "m9 17-5-5 5-5M20 18v-2a4 4 0 0 0-4-4H4",
	  rocket: "M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09ZM12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2ZM9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5",
	  settings: "M9.7 4.1a2.34 2.34 0 0 1 4.6 0 2.34 2.34 0 0 0 3.3 1.9 2.34 2.34 0 0 1 2.4 4.1 2.34 2.34 0 0 0 0 3.8 2.34 2.34 0 0 1-2.4 4.1 2.34 2.34 0 0 0-3.3 1.9 2.34 2.34 0 0 1-4.6 0A2.34 2.34 0 0 0 6.4 18 2.34 2.34 0 0 1 4 13.9a2.34 2.34 0 0 0 0-3.8A2.34 2.34 0 0 1 6.4 6a2.34 2.34 0 0 0 3.3-1.9ZM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z",
	  search: "M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-2.3 5.7L21 21",
	  shield: "M12 2 21 5v6c0 5.7-3.7 9.4-9 11-5.3-1.6-9-5.3-9-11V5l9-3Zm0 3L6 7v4c0 3.9 2.2 6.5 6 8 3.8-1.5 6-4.1 6-8V7l-6-2Z",
	  sparkles: "M12 3l1.2 3.8L17 8l-3.8 1.2L12 13l-1.2-3.8L7 8l3.8-1.2L12 3ZM19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8L19 14ZM5 14l.8 2.2L8 17l-2.2.8L5 20l-.8-2.2L2 17l2.2-.8L5 14Z",
	  square: "M3 3h18v18H3z",
	  "user-plus": "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm10-3v6m3-3h-6",
	  upload: "M12 15V3m-5 5 5-5 5 5M5 21h14",
	  x: "M18 6 6 18M6 6l12 12"
	}), ICON_MARKUP = Object.freeze({
	  at: '<path d="M12 2a10 10 0 1 0 5.8 18.2l-1.3-1.7A7.8 7.8 0 1 1 19.8 12v1.2c0 1.2-.5 1.8-1.4 1.8-.8 0-1.3-.5-1.3-1.5V8h-2v1A5 5 0 1 0 16 16c.7.8 1.6 1.2 2.7 1.2 2.1 0 3.3-1.5 3.3-4V12c0-5.5-4.5-10-10-10Zm0 12.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z" fill="currentColor" stroke="none" fill-rule="evenodd"/>',
	  boost: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2Z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
	  database: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>',
	  flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1Z"/><path d="M4 22v-7"/>',
	  "floating-window": '<rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 9h16M7 7h.01M10 7h.01"/>',
	  flask: '<path d="M9 3h6M10 3v6l-5 9a2 2 0 0 0 1.7 3h10.6a2 2 0 0 0 1.7-3l-5-9V3M7.5 14h9"/>',
	  "git-branch": '<path d="M6 3v12"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
	  "header-settings": '<path d="M4 6h5M13 6h7"/><circle cx="11" cy="6" r="2"/><path d="M4 12h10M18 12h2"/><circle cx="16" cy="12" r="2"/><path d="M4 18h2M10 18h10"/><circle cx="8" cy="18" r="2"/>',
	  history: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
	  image: '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/>',
	  "layout-grid": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M12 4v16M3 12h18"/>',
	  lock: '<rect width="14" height="10" x="5" y="11" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
	  mail: '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
	  "menu-box": '<rect x="3" y="3" width="18" height="18" rx="3"/><path d="M3 12h18M9 8h6M9 16h6"/>',
	  monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
	  moon: '<path d="M20.99 12.8A9 9 0 1 1 11.2 3.01 7 7 0 0 0 20.99 12.8Z"/>',
	  "panel-left": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>',
	  "panel-right": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M15 4v16"/>',
	  palette: '<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" stroke="none"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor" stroke="none"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor" stroke="none"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor" stroke="none"/><path d="M12 2a10 10 0 0 0 0 20c1.1 0 2-.9 2-2 0-.5-.2-1-.6-1.4-.4-.4-.6-.9-.6-1.4a2 2 0 0 1 2-2H17a5 5 0 0 0 5-5C22 5.7 17.5 2 12 2Z"/>',
	  pin: '<path d="M12 17v5M5 17h14m-13-14 1 7-3 3h16l-3-3 1-7Z"/>',
	  share: '<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/><path d="m16 6-4-4-4 4M12 2v13"/>',
	  "select-items": '<rect x="7" y="7" width="14" height="14" rx="3"/><path d="M3 15V6a3 3 0 0 1 3-3h9"/>',
	  "select-items-check": '<rect x="7" y="7" width="14" height="14" rx="3"/><path d="M3 15V6a3 3 0 0 1 3-3h9M10.5 14l2 2 4-4"/>',
	  "shield-halved": '<path d="M12 2 4 5v6c0 5 3.3 9.4 8 11 4.7-1.6 8-6 8-11V5l-8-3Z"/><path d="M12 2 4 5v6c0 5 3.3 9.4 8 11V2Z" fill="currentColor" stroke="none"/>',
	  smile: '<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/>',
	  sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>',
	  tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor" stroke="none"/>',
	  trash: '<path d="M3 6h18M8 6V4h8v2m3 0-1 14H6L5 6M10 11v5M14 11v5"/>',
	  "trash-2": '<path d="M4 6h16M9 6V4h6v2"/><path d="m7 6 .8 14h8.4L17 6M10 10v6M14 10v6"/>',
	  type: '<path d="M4 7V4h16v3M9 20h6M12 4v16"/>',
	  unlock: '<rect width="14" height="10" x="5" y="11" rx="2"/><path d="M8 11V7a4 4 0 0 1 7.9-1"/>',
	  "user-round": '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
	  wrench: '<path d="M14.7 6.3a4 4 0 0 0-5-5L7.4 3.6l3 3-3.8 3.8-3-3-2.3 2.3a4 4 0 0 0 5 5L15.6 24l4-4-8.7-9.3 3.8-4.4Z"/>'
	}), ICON_TEMPLATES = /* @__PURE__ */ new WeakMap();
	function hasReaderIcon(name) {
	  return !!(ICON_PATHS[name] || ICON_MARKUP[name]);
	}
	function readerIconSvgMarkup(name) {
	  const pathData = ICON_PATHS[name], markup = ICON_MARKUP[name];
	  if (!pathData && !markup) throw new Error(`未知 Reader 图标:${name}`);
	  const content = pathData ? `<path d="${pathData}"></path>` : markup;
	  return `<svg class="ldp-icon ldp-icon-${name}" data-icon="${name}" data-ldp-reader-icon="" viewBox="0 0 24 24" aria-hidden="true" focusable="false">${content}</svg>`;
	}
	function selfContainedNativeIcon(node) {
	  if (node.nodeType !== 1) return !0;
	  const element = node;
	  return element.querySelector("use") ? element.querySelector(
	    "path,circle,ellipse,line,polyline,polygon,rect,g"
	  ) !== null : !0;
	}
	function createReaderIcon(document, name, extraClass = "") {
	  const pathData = ICON_PATHS[name], markup = ICON_MARKUP[name];
	  if (!pathData && !markup) throw new Error(`未知 Reader 图标:${name}`);
	  let templates = ICON_TEMPLATES.get(document);
	  templates || (templates = /* @__PURE__ */ new Map(), ICON_TEMPLATES.set(document, templates));
	  let template = templates.get(name);
	  if (!template) {
	    if (template = document.createElementNS(
	      SVG_NAMESPACE,
	      "svg"
	    ), template.classList.add("ldp-icon"), template.classList.add(`ldp-icon-${name}`), template.dataset.icon = name, template.dataset.ldpReaderIcon = "", template.setAttribute("viewBox", "0 0 24 24"), template.setAttribute("aria-hidden", "true"), template.setAttribute("focusable", "false"), pathData) {
	      const path = document.createElementNS(SVG_NAMESPACE, "path");
	      path.setAttribute("d", pathData), template.append(path);
	    } else
	      template.innerHTML = markup;
	    templates.set(name, template);
	  }
	  const svg = template.cloneNode(!0);
	  for (const className of extraClass.split(/\s+/).filter(Boolean))
	    svg.classList.add(className);
	  return svg;
	}
	function resolveReaderIcon(document, name, nativeIcon = null) {
	  if (hasReaderIcon(name)) return createReaderIcon(document, name);
	  if (nativeIcon && selfContainedNativeIcon(nativeIcon)) return nativeIcon;
	  const fallback = createReaderIcon(document, "circle-help");
	  return fallback.dataset.readerIconFallbackFor = name, fallback;
	}
	function renderReaderIcon(document, name, renderer) {
	  let rendered = null;
	  try {
	    rendered = renderer?.(name, document) ?? null;
	  } catch {
	  }
	  return resolveReaderIcon(document, name, rendered);
	}
}, "44c88f76d9ed017c46ff4443d6d9668e460663d76eef84e2f284ca9d736d825b");

/* Source: lite/src/components/reader-image-fallback.ts */
runtime.register("src/components/reader-image-fallback.js", function(module, exports, require) {
	var reader_image_fallback_exports = {};
	__export(reader_image_fallback_exports, {
	  installReaderImageSourceFallback: () => installReaderImageSourceFallback,
	  installReaderSiteLogoFallback: () => installReaderSiteLogoFallback,
	  replaceImageWithFallbackOnError: () => replaceImageWithFallbackOnError
	});
	module.exports = __toCommonJS(reader_image_fallback_exports);
	function replaceImageWithFallbackOnError(image, createFallback) {
	  image.addEventListener("error", () => {
	    image.parentNode && image.replaceWith(createFallback());
	  }, { once: !0 });
	}
	function installReaderImageSourceFallback(image, sources, createFallback, recoverSource, visibleSource) {
	  const candidates = [...new Set(
	    sources.map((source) => String(source).trim()).filter(Boolean)
	  )];
	  let directIndex = 0, recoveryIndex = 0, fallback = null, recovering = !1;
	  const showFallback = () => {
	    if (fallback?.parentNode) return fallback;
	    const next = createFallback();
	    return image.parentNode && image.replaceWith(next), fallback = next, next.parentNode ? next : null;
	  }, recoverNext = async () => {
	    if (!recovering) {
	      for (recovering = !0; recoverSource && recoveryIndex < candidates.length; ) {
	        const candidate = candidates[recoveryIndex];
	        recoveryIndex += 1;
	        try {
	          const recovered = String(await recoverSource(candidate)).trim();
	          if (!fallback?.parentNode && !image.parentNode) return;
	          if (!recovered) continue;
	          fallback?.parentNode && (fallback.replaceWith(image), fallback = null), recovering = !1, image.src = recovered;
	          return;
	        } catch {
	        }
	      }
	      recovering = !1, fallback?.parentNode && image.removeEventListener("error", advance);
	    }
	  };
	  function advance() {
	    if (recoverSource) {
	      if (!showFallback()) return;
	      recoverNext();
	      return;
	    }
	    const direct = candidates[directIndex];
	    if (directIndex += 1, direct) {
	      image.src = direct;
	      return;
	    }
	    image.removeEventListener("error", advance), showFallback();
	  }
	  if (image.addEventListener("error", advance), recoverSource) {
	    const visible = String(visibleSource ?? "").trim();
	    if (visible) image.src = visible;
	    else if (!showFallback()) return;
	    recoverNext();
	  } else
	    advance();
	}
	const READER_SITE_LOGO_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent(
	  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#e9eef3"/><path d="M18 33a14 14 0 1 1 28 0v13H18V33Z" fill="#748392"/><circle cx="27" cy="31" r="3" fill="#fff"/><circle cx="37" cy="31" r="3" fill="#fff"/></svg>'
	)}`;
	function siteFaviconSource(image, primarySource) {
	  const documentOrigin = String(
	    image.ownerDocument.location?.origin ?? ""
	  ).trim();
	  for (const base of [documentOrigin, primarySource])
	    if (base)
	      try {
	        const url = new URL("/favicon.ico", base);
	        if (url.protocol === "https:" || url.protocol === "http:")
	          return url.href;
	      } catch {
	      }
	  return "";
	}
	function installReaderSiteLogoFallback(image, primarySource) {
	  const primary = String(primarySource).trim(), sources = [...new Set([
	    primary,
	    siteFaviconSource(image, primary),
	    READER_SITE_LOGO_PLACEHOLDER
	  ].filter(Boolean))];
	  let index = 0;
	  const advance = () => {
	    index += 1;
	    const next = sources[index];
	    if (!next) {
	      image.removeEventListener("error", advance);
	      return;
	    }
	    image.src = next;
	  };
	  image.addEventListener("error", advance), image.src = sources[0] ?? READER_SITE_LOGO_PLACEHOLDER;
	}
}, "4da4b336ae2253b31010cb90068ad1856c50c2996fdea5cbefdb34781e12ee37");

/* Source: lite/src/dom/event-target.ts */
runtime.register("src/dom/event-target.js", function(module, exports, require) {
	var event_target_exports = {};
	__export(event_target_exports, {
	  deepActiveElement: () => deepActiveElement,
	  eventElement: () => eventElement,
	  eventPath: () => eventPath,
	  eventPathIncludes: () => eventPathIncludes,
	  usesNativeLinkNavigation: () => usesNativeLinkNavigation
	});
	module.exports = __toCommonJS(event_target_exports);
	function eventPath(event) {
	  try {
	    const path = event.composedPath?.();
	    if (path?.length) return path;
	  } catch {
	  }
	  return event.target ? Object.freeze([event.target]) : Object.freeze([]);
	}
	function eventElement(event) {
	  for (const target of eventPath(event))
	    if (target !== null && typeof target == "object" && target.nodeType === 1) return target;
	  return null;
	}
	function eventPathIncludes(event, node) {
	  if (!node) return !1;
	  if (eventPath(event).includes(node)) return !0;
	  const target = event.target;
	  return target !== null && typeof target == "object" && typeof target.nodeType == "number" && node.contains(target);
	}
	function usesNativeLinkNavigation(event) {
	  return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
	}
	function deepActiveElement(document) {
	  let active = document.activeElement;
	  for (; active?.shadowRoot?.activeElement; )
	    active = active.shadowRoot.activeElement;
	  return active;
	}
}, "7e486dde4a1615d7509486f91752765d59762d7ee30e373d3a730e51ec3f997d");

/* Source: lite/src/dom/floating-surface-wheel.ts */
runtime.register("src/dom/floating-surface-wheel.js", function(module, exports, require) {
	var floating_surface_wheel_exports = {};
	__export(floating_surface_wheel_exports, {
	  bindFloatingSurfaceWheel: () => bindFloatingSurfaceWheel,
	  containFloatingSurfaceWheel: () => containFloatingSurfaceWheel
	});
	module.exports = __toCommonJS(floating_surface_wheel_exports);
	var import_event_target = require("./event-target.js");
	const DOCUMENT_WHEEL_OPTIONS = {
	  capture: !0,
	  passive: !1
	}, boundFloatingSurfaces = /* @__PURE__ */ new WeakMap();
	function defaultStyle(element) {
	  const view = element.ownerDocument.defaultView;
	  return typeof view?.getComputedStyle == "function" ? view.getComputedStyle(element) : { overflowX: "", overflowY: "" };
	}
	function wheelDelta(event, surface) {
	  return event.deltaMode === 1 ? { x: event.deltaX * 40, y: event.deltaY * 40 } : event.deltaMode === 2 ? {
	    x: event.deltaX * surface.clientWidth,
	    y: event.deltaY * surface.clientHeight
	  } : { x: event.deltaX, y: event.deltaY };
	}
	function containFloatingSurfaceWheel(surface, event, environment = {}) {
	  event.stopPropagation();
	  const target = (0, import_event_target.eventElement)(event), { x: deltaX, y: deltaY } = wheelDelta(event, surface);
	  if (!target || !surface.contains(target)) {
	    (deltaX || deltaY) && event.preventDefault();
	    return;
	  }
	  const style = environment.style ?? defaultStyle;
	  let scrollTarget = target;
	  for (; scrollTarget && surface.contains(scrollTarget); ) {
	    const computed = style(scrollTarget), maxScrollLeft = scrollTarget.scrollWidth - scrollTarget.clientWidth, maxScrollTop = scrollTarget.scrollHeight - scrollTarget.clientHeight, canScrollX = !!(deltaX && maxScrollLeft > 1 && /(auto|scroll|overlay)/.test(computed.overflowX) && (deltaX < 0 ? scrollTarget.scrollLeft > 0 : scrollTarget.scrollLeft < maxScrollLeft - 1)), canScrollY = !!(deltaY && maxScrollTop > 1 && /(auto|scroll|overlay)/.test(computed.overflowY) && (deltaY < 0 ? scrollTarget.scrollTop > 0 : scrollTarget.scrollTop < maxScrollTop - 1));
	    if (canScrollX || canScrollY) return;
	    if (scrollTarget === surface) break;
	    scrollTarget = scrollTarget.parentElement;
	  }
	  (deltaX || deltaY) && event.preventDefault();
	}
	function floatingSurfaceAtWheelPoint(binding, event) {
	  const x = Number(event.clientX), y = Number(event.clientY);
	  if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
	  const candidates = [...binding.surfaces].filter(([surface, count]) => count.latchedTarget > 0 && !surface.hidden && surface.isConnected).reverse(), hits = /* @__PURE__ */ new Map();
	  let canHitTest = !1;
	  for (const [surface] of candidates) {
	    const root = surface.getRootNode(), tester = typeof root.elementFromPoint == "function" ? root.elementFromPoint.bind(root) : typeof binding.document.elementFromPoint == "function" ? binding.document.elementFromPoint.bind(binding.document) : null;
	    if (!tester) continue;
	    canHitTest = !0;
	    let hit = hits.get(root);
	    if (hits.has(root) || (hit = tester(x, y), hits.set(root, hit ?? null)), hit && (hit === surface || surface.contains(hit))) return surface;
	  }
	  if (canHitTest) return null;
	  for (const [surface] of candidates) {
	    const rect = surface.getBoundingClientRect();
	    if (rect.width > 0 && rect.height > 0 && x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return surface;
	  }
	  return null;
	}
	function installDocumentBinding(document) {
	  let binding;
	  const onWheel = (eventValue) => {
	    const event = eventValue;
	    for (const surface2 of binding.surfaces.keys())
	      if ((0, import_event_target.eventPathIncludes)(event, surface2)) return;
	    const surface = floatingSurfaceAtWheelPoint(binding, event);
	    surface && (containFloatingSurfaceWheel(surface, event), event.stopImmediatePropagation());
	  };
	  return binding = {
	    document,
	    surfaces: /* @__PURE__ */ new Map(),
	    onWheel
	  }, document.addEventListener("wheel", onWheel, DOCUMENT_WHEEL_OPTIONS), boundFloatingSurfaces.set(document, binding), binding;
	}
	function bindFloatingSurfaceWheel(surface, options = {}) {
	  const surfaceOptions = {
	    capture: options.capture === !0,
	    passive: !1
	  }, onSurfaceWheel = (event) => {
	    containFloatingSurfaceWheel(surface, event);
	  };
	  surface.addEventListener("wheel", onSurfaceWheel, surfaceOptions);
	  const document = surface.ownerDocument, binding = boundFloatingSurfaces.get(document) ?? installDocumentBinding(document), count = binding.surfaces.get(surface) ?? {
	    total: 0,
	    latchedTarget: 0
	  };
	  count.total += 1, options.captureLatchedTarget !== !1 && (count.latchedTarget += 1), binding.surfaces.set(surface, count);
	  let active = !0;
	  return () => {
	    if (!active) return;
	    active = !1, surface.removeEventListener("wheel", onSurfaceWheel, surfaceOptions);
	    const current = binding.surfaces.get(surface);
	    current && (current.total -= 1, options.captureLatchedTarget !== !1 && (current.latchedTarget -= 1), current.total <= 0 && binding.surfaces.delete(surface)), binding.surfaces.size || (document.removeEventListener(
	      "wheel",
	      binding.onWheel,
	      DOCUMENT_WHEEL_OPTIONS
	    ), boundFloatingSurfaces.delete(document));
	  };
	}
}, "cb4847a740a6e3124810384f7963c536d57456e9834e71bfdde2e590260fca8d");

/* Source: lite/src/dom/html-element.ts */
runtime.register("src/dom/html-element.js", function(module, exports, require) {
	var html_element_exports = {};
	__export(html_element_exports, {
	  htmlElement: () => htmlElement
	});
	module.exports = __toCommonJS(html_element_exports);
	function htmlElement(document, tagName, className = "", textContent) {
	  const node = document.createElement(tagName);
	  return node.className = className, textContent !== void 0 && (node.textContent = textContent), node;
	}
}, "67cadf2903ac7d3c55b0e65cd9828d3b33fcc50916b623b29b38b675f72b3d09");

/* Source: lite/src/dom/post-view.ts */
runtime.register("src/dom/post-view.js", function(module, exports, require) {
	var post_view_exports = {};
	__export(post_view_exports, {
	  PostView: () => PostView
	});
	module.exports = __toCommonJS(post_view_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_identifiers = require("../discourse/identifiers.js"), import_html_element = require("./html-element.js");
	const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
	class PostView {
	  identity;
	  scope;
	  slots;
	  #unbindActionManifest = null;
	  constructor(document, identity, parentScope) {
	    const postId = (0, import_identifiers.discoursePostId)(identity.postId), postNumber = (0, import_identifiers.discoursePostNumber)(identity.postNumber);
	    this.identity = Object.freeze({ ...identity, postId, postNumber }), this.scope = import_lifecycle.LifecycleScope.ownedBy(parentScope);
	    const root = (0, import_html_element.htmlElement)(document, "article", "ldp-post");
	    root.dataset.postId = String(postId), root.dataset.postNumber = String(postNumber), root.dataset.username = identity.username, identity.createdAt && (root.dataset.createdAt = identity.createdAt);
	    const header = (0, import_html_element.htmlElement)(document, "header", "ldp-post-head"), body = (0, import_html_element.htmlElement)(document, "div", "ldp-post-body"), content = (0, import_html_element.htmlElement)(document, "div", "ldp-content cooked"), bodyLayer = (0, import_html_element.htmlElement)(document, "div", "ldp-post-body-layer"), boost = (0, import_html_element.htmlElement)(document, "div", "ldp-boost-list");
	    boost.hidden = !0;
	    const actions = (0, import_html_element.htmlElement)(document, "div", "ldp-reactions ldp-post-actions"), topicFooter = (0, import_html_element.htmlElement)(document, "footer", "ldp-topic-footer-slot");
	    topicFooter.hidden = !0, body.append(content, bodyLayer, boost, actions, topicFooter);
	    const replyTree = (0, import_html_element.htmlElement)(document, "section", "ldp-children ldp-reply-tree"), branchOverlay = document.createElementNS(SVG_NAMESPACE, "svg");
	    branchOverlay.classList.add("ldp-branch-overlay"), branchOverlay.setAttribute("hidden", ""), branchOverlay.setAttribute("aria-hidden", "true"), branchOverlay.setAttribute("focusable", "false");
	    const visiblePath = document.createElementNS(SVG_NAMESPACE, "path");
	    visiblePath.classList.add("ldp-branch-visible-path");
	    const hitPath = document.createElementNS(SVG_NAMESPACE, "path");
	    hitPath.classList.add("ldp-branch-hit-path"), branchOverlay.append(visiblePath, hitPath);
	    const replyControls = (0, import_html_element.htmlElement)(document, "div", "ldp-reply-controls ldp-sub-actions"), replyList = (0, import_html_element.htmlElement)(document, "div", "ldp-reply-list");
	    replyTree.append(branchOverlay, replyList, replyControls), root.append(header, body, replyTree), this.slots = Object.freeze({
	      root,
	      header,
	      body,
	      content,
	      bodyLayer,
	      boost,
	      actions,
	      topicFooter,
	      replyTree,
	      branchOverlay,
	      replyControls,
	      replyList
	    });
	  }
	  get postNumber() {
	    return this.identity.postNumber;
	  }
	  setTreePosition(parentPostNumber, depth) {
	    if (parentPostNumber !== null && (0, import_identifiers.discoursePostNumber)(parentPostNumber), depth !== void 0 && (!Number.isSafeInteger(depth) || depth < 0))
	      throw new RangeError("depth 必须是非负安全整数或 undefined");
	    const parentValue = parentPostNumber === null ? void 0 : String(parentPostNumber);
	    parentValue === void 0 ? this.slots.root.dataset.parentPostNumber !== void 0 && delete this.slots.root.dataset.parentPostNumber : this.slots.root.dataset.parentPostNumber !== parentValue && (this.slots.root.dataset.parentPostNumber = parentValue);
	    const depthValue = depth === void 0 ? void 0 : String(depth);
	    depthValue === void 0 ? this.slots.root.dataset.ldpNestDepth !== void 0 && delete this.slots.root.dataset.ldpNestDepth : this.slots.root.dataset.ldpNestDepth !== depthValue && (this.slots.root.dataset.ldpNestDepth = depthValue);
	    const nested = depth !== void 0 && depth > 0;
	    this.slots.root.classList.contains("ldp-nested-preview") !== nested && this.slots.root.classList.toggle("ldp-nested-preview", nested), nested && this.slots.root.classList.contains("ldp-zebra-alt") && this.slots.root.classList.remove("ldp-zebra-alt");
	  }
	  /**
	   * 将普通、嵌套、实时新增和回屏楼层接到同一份动作状态。
	   *
	   * PostView 只提供命名槽位与生命周期;具体按钮 DOM 仍由 renderer 组件拥有。
	   */
	  bindActionManifest(source, renderer) {
	    this.#unbindActionManifest?.(), renderer(this.slots, source.snapshot());
	    const unsubscribe = source.subscribe((snapshot) => {
	      renderer(this.slots, snapshot);
	    }, this.scope);
	    let active = !0;
	    const cleanup = () => {
	      active && (active = !1, unsubscribe(), this.#unbindActionManifest === cleanup && (this.#unbindActionManifest = null));
	    };
	    return this.#unbindActionManifest = cleanup, this.scope.add(cleanup), cleanup;
	  }
	  destroy() {
	    try {
	      this.scope.destroy();
	    } finally {
	      this.slots.root.remove();
	    }
	  }
	}
}, "8687ab8a5ffb375a62325deed0d15b34a1366e58f9157f99a6e9ba24c1a30edf");

/* Source: lite/src/dom/reply-tree-dom-owner.ts */
runtime.register("src/dom/reply-tree-dom-owner.js", function(module, exports, require) {
	var reply_tree_dom_owner_exports = {};
	__export(reply_tree_dom_owner_exports, {
	  ReplyTreeDomOwner: () => ReplyTreeDomOwner
	});
	module.exports = __toCommonJS(reply_tree_dom_owner_exports);
	function directPostNumber(element) {
	  const raw = element.getAttribute("data-post-number");
	  if (raw === null) return null;
	  const postNumber = Number(raw);
	  return Number.isSafeInteger(postNumber) ? postNumber : null;
	}
	function insertPostInOrder(container, view) {
	  const root = view.slots.root;
	  if (root.parentElement === container) {
	    let previousPostNumber = null;
	    for (let previous = root.previousElementSibling; previous && (previousPostNumber = directPostNumber(previous), previousPostNumber === null); previous = previous.previousElementSibling)
	      ;
	    let nextPostNumber = null;
	    for (let next2 = root.nextElementSibling; next2 && (nextPostNumber = directPostNumber(next2), nextPostNumber === null); next2 = next2.nextElementSibling)
	      ;
	    if ((previousPostNumber === null || previousPostNumber < view.postNumber) && (nextPostNumber === null || nextPostNumber > view.postNumber)) return;
	  }
	  const next = Array.from(container.children).find((candidate) => {
	    const postNumber = directPostNumber(candidate);
	    return postNumber !== null && postNumber > view.postNumber;
	  });
	  container.insertBefore(root, next ?? null);
	}
	function virtualSpacer(document, blockSize) {
	  const spacer = document.createElement("div");
	  return spacer.className = "ldp-tree-virtual-spacer", spacer.setAttribute("aria-hidden", "true"), spacer.style.blockSize = `${Math.max(0, blockSize)}px`, spacer;
	}
	function removeVirtualSpacers(container) {
	  for (const child of Array.from(container.children))
	    child.classList.contains("ldp-tree-virtual-spacer") && child.remove();
	}
	function childLayoutKey(layout) {
	  return `${layout.postNumbers.join(".")}@${layout.beforeSizes.map((size) => size.toFixed(2)).join(".")}@` + layout.afterSize.toFixed(2);
	}
	class ReplyTreeDomOwner {
	  topology;
	  rootList;
	  #views = /* @__PURE__ */ new Map();
	  #viewRevision = 0;
	  #lastSyncKey = "";
	  #lastCommit = null;
	  #childLayoutKeys = /* @__PURE__ */ new Map();
	  constructor(topology, rootList) {
	    this.topology = topology, this.rootList = rootList;
	  }
	  register(view, sync = !0) {
	    const existing = this.#views.get(view.postNumber);
	    if (existing && existing !== view)
	      throw new Error(`楼层 #${view.postNumber} 已由另一个 PostView 持有`);
	    this.#views.set(view.postNumber, view);
	    const parentPostNumber = this.topology.parentOf(view.postNumber);
	    parentPostNumber != null && this.#childLayoutKeys.delete(parentPostNumber), this.#viewRevision += 1, this.#lastSyncKey = "", sync && this.sync();
	  }
	  view(postNumber) {
	    return this.#views.get(postNumber);
	  }
	  views() {
	    return Object.freeze([...this.#views.values()]);
	  }
	  willSyncChange(mountedRootPostNumbers, mountPlan) {
	    return !this.#lastCommit || this.#syncKey(mountedRootPostNumbers, mountPlan) !== this.#lastSyncKey;
	  }
	  unregister(postNumber, destroy = !0, resync = !0) {
	    const view = this.#views.get(postNumber);
	    if (!view) return;
	    this.#views.delete(postNumber), this.#childLayoutKeys.delete(postNumber);
	    const parentPostNumber = this.topology.parentOf(postNumber);
	    return parentPostNumber != null && this.#childLayoutKeys.delete(parentPostNumber), this.#viewRevision += 1, this.#lastSyncKey = "", destroy ? view.destroy() : view.slots.root.remove(), resync && this.sync(), view;
	  }
	  sync(mountedRootPostNumbers, mountPlan) {
	    const syncKey = this.#syncKey(mountedRootPostNumbers, mountPlan);
	    if (this.#lastCommit && syncKey === this.#lastSyncKey)
	      return Object.freeze({ ...this.#lastCommit, changed: !1 });
	    const mountedRoots = [], mountedReplies = [], parked = [], missingParents = [], viewEntries = [...this.#views.values()].map((view) => ({
	      view,
	      parentPostNumber: this.topology.parentOf(view.postNumber),
	      depth: this.topology.depthOf(view.postNumber),
	      rootPostNumber: this.topology.rootOf(view.postNumber)
	    })).sort(
	      (left, right) => (left.depth ?? Number.MAX_SAFE_INTEGER) - (right.depth ?? Number.MAX_SAFE_INTEGER) || left.view.postNumber - right.view.postNumber
	    );
	    for (const {
	      view,
	      parentPostNumber,
	      depth,
	      rootPostNumber
	    } of viewEntries) {
	      const selectedByNodeWindow = !mountPlan || mountPlan.mountedPostNumbers.has(view.postNumber), ancestorShell = !!mountPlan && mountPlan.shellPostNumbers.has(view.postNumber);
	      view.slots.root.classList.contains("ldp-virtual-ancestor-shell") !== ancestorShell && view.slots.root.classList.toggle(
	        "ldp-virtual-ancestor-shell",
	        ancestorShell
	      );
	      const ownSize = mountPlan?.ownSizes.get(view.postNumber);
	      if (ownSize === void 0)
	        view.slots.root.style.getPropertyValue("--ldp-virtual-own-size") && view.slots.root.style.removeProperty("--ldp-virtual-own-size");
	      else {
	        const ownSizeValue = `${Math.max(0, ownSize)}px`;
	        view.slots.root.style.getPropertyValue("--ldp-virtual-own-size") !== ownSizeValue && view.slots.root.style.setProperty(
	          "--ldp-virtual-own-size",
	          ownSizeValue
	        );
	      }
	      if (parentPostNumber === void 0) {
	        view.slots.root.remove(), this.#childLayoutKeys.delete(view.postNumber), view.setTreePosition(null, void 0), parked.push(view.postNumber);
	        continue;
	      }
	      if (!selectedByNodeWindow || mountedRootPostNumbers && (rootPostNumber === void 0 || !mountedRootPostNumbers.has(rootPostNumber))) {
	        view.slots.root.remove(), this.#childLayoutKeys.delete(view.postNumber), view.setTreePosition(parentPostNumber, depth), parked.push(view.postNumber);
	        continue;
	      }
	      if (parentPostNumber === null) {
	        view.setTreePosition(null, depth), insertPostInOrder(this.rootList, view), mountedRoots.push(view.postNumber);
	        continue;
	      }
	      const parent = this.#views.get(parentPostNumber);
	      if (view.setTreePosition(parentPostNumber, depth), !parent) {
	        view.slots.root.remove(), this.#childLayoutKeys.delete(view.postNumber), parked.push(view.postNumber), missingParents.push(parentPostNumber);
	        continue;
	      }
	      insertPostInOrder(parent.slots.replyList, view), mountedReplies.push(view.postNumber);
	    }
	    if (mountPlan)
	      for (const { view } of viewEntries) {
	        if (!mountPlan.mountedPostNumbers.has(view.postNumber)) continue;
	        const childLayout = mountPlan.childLayouts.get(view.postNumber);
	        if (!childLayout) {
	          this.#childLayoutKeys.has(view.postNumber) && (removeVirtualSpacers(view.slots.replyList), this.#childLayoutKeys.delete(view.postNumber));
	          continue;
	        }
	        const nextLayoutKey = childLayoutKey(childLayout);
	        if (this.#childLayoutKeys.get(view.postNumber) === nextLayoutKey)
	          continue;
	        removeVirtualSpacers(view.slots.replyList);
	        const fragment = view.slots.replyList.ownerDocument.createDocumentFragment();
	        for (let index = 0; index < childLayout.postNumbers.length; index += 1) {
	          const beforeSize = childLayout.beforeSizes[index] ?? 0;
	          beforeSize > 0 && fragment.append(virtualSpacer(
	            view.slots.replyList.ownerDocument,
	            beforeSize
	          ));
	          const childPostNumber = childLayout.postNumbers[index], childView = this.#views.get(childPostNumber);
	          childView && mountPlan.mountedPostNumbers.has(childPostNumber) && fragment.append(childView.slots.root);
	        }
	        childLayout.afterSize > 0 && fragment.append(virtualSpacer(
	          view.slots.replyList.ownerDocument,
	          childLayout.afterSize
	        )), view.slots.replyList.append(fragment), this.#childLayoutKeys.set(view.postNumber, nextLayoutKey);
	      }
	    else if (this.#childLayoutKeys.size) {
	      for (const postNumber of this.#childLayoutKeys.keys()) {
	        const view = this.#views.get(postNumber);
	        view && removeVirtualSpacers(view.slots.replyList);
	      }
	      this.#childLayoutKeys.clear();
	    }
	    const commit = Object.freeze({
	      changed: !0,
	      mountedRoots: Object.freeze(mountedRoots),
	      mountedReplies: Object.freeze(mountedReplies),
	      parked: Object.freeze(parked),
	      missingParents: Object.freeze([...new Set(missingParents)].sort((a, b) => a - b))
	    });
	    return this.#lastSyncKey = syncKey, this.#lastCommit = commit, commit;
	  }
	  #syncKey(mountedRootPostNumbers, mountPlan) {
	    const roots = mountedRootPostNumbers ? [...mountedRootPostNumbers].sort((left, right) => left - right).join(",") : "*";
	    if (!mountPlan)
	      return `${String(this.topology.revision ?? "")}|${this.#viewRevision}|${roots}`;
	    const mounted = [...mountPlan.mountedPostNumbers].sort((left, right) => left - right).join(","), shells = [...mountPlan.shellPostNumbers].sort((left, right) => left - right).join(","), ownSizes = [...mountPlan.ownSizes].sort(([left], [right]) => left - right).map(([postNumber, size]) => `${postNumber}:${size.toFixed(2)}`).join(","), layouts = [...mountPlan.childLayouts].sort(([left], [right]) => left - right).map(
	      ([postNumber, layout]) => `${postNumber}:${layout.postNumbers.join(".")}@${layout.beforeSizes.map((size) => size.toFixed(2)).join(".")}@` + layout.afterSize.toFixed(2)
	    ).join("|");
	    return `${String(this.topology.revision ?? "")}|${this.#viewRevision}|${roots}|${mounted}|${shells}|${ownSizes}|${layouts}`;
	  }
	  destroy() {
	    const views = [...this.#views.values()].sort((left, right) => right.postNumber - left.postNumber);
	    this.#views.clear(), this.#childLayoutKeys.clear(), this.#lastCommit = null, this.#lastSyncKey = "";
	    for (const view of views) view.destroy();
	  }
	}
}, "5cd439f571d7a08ffebc93392e187f70af6b95d33b71a229b088f1c3f74bb8d7");

/* Source: lite/src/dom/reply-tree-repository.ts */
runtime.register("src/dom/reply-tree-repository.js", function(module, exports, require) {
	var reply_tree_repository_exports = {};
	__export(reply_tree_repository_exports, {
	  ReplyTreeRepository: () => ReplyTreeRepository
	});
	module.exports = __toCommonJS(reply_tree_repository_exports);
	var import_signal = require("../kernel/signal.js"), import_identifiers = require("../discourse/identifiers.js"), import_ingest_version = require("../discourse/ingest-version.js"), import_reply_tree = require("./reply-tree.js");
	function relationFromPost(post) {
	  const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(post.post_number);
	  if (postNumber === null) return null;
	  const parentPostNumber = (0, import_identifiers.tryDiscoursePostNumber)(post.reply_to_post_number);
	  return Object.freeze({ postNumber, parentPostNumber });
	}
	function assertExpectedPostCount(value) {
	  if (!Number.isSafeInteger(value) || value < 0)
	    throw new RangeError("expectedPostCount 必须是非负安全整数");
	  return value;
	}
	function validSource(value) {
	  return (0, import_ingest_version.normalizeDiscourseIngestSource)(value) !== null;
	}
	function shouldApplyRelation(current, observedAt, source) {
	  return (0, import_ingest_version.shouldReplaceDiscourseVersion)(current, { observedAt, source });
	}
	function assertStoredSnapshot(value, topicId) {
	  if (value === null) return null;
	  if (value.schemaVersion !== 2 || value.topicId !== topicId || !Number.isFinite(value.savedAt) || !Number.isSafeInteger(value.expectedPostCount) || value.expectedPostCount < 0 || !value.tree || !Array.isArray(value.tree.relations) || !Array.isArray(value.versions))
	    throw new Error(`Topic ${topicId} 的回复树快照无效`);
	  const relationPostNumbers = new Set(value.tree.relations.map((relation) => relation.postNumber)), versionPostNumbers = /* @__PURE__ */ new Set();
	  for (const version of value.versions) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(version?.postNumber);
	    if (postNumber === null || versionPostNumbers.has(postNumber) || !relationPostNumbers.has(postNumber) || !Number.isFinite(version.observedAt) || version.observedAt < 0 || !validSource(version.source))
	      throw new Error(`Topic ${topicId} 的回复树关系版本无效`);
	    versionPostNumbers.add(postNumber);
	  }
	  if (versionPostNumbers.size !== relationPostNumbers.size)
	    throw new Error(`Topic ${topicId} 的回复树关系版本不完整`);
	  const removedPostNumbers = /* @__PURE__ */ new Set();
	  for (const version of value.removedVersions ?? []) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(version?.postNumber);
	    if (postNumber === null || removedPostNumbers.has(postNumber) || relationPostNumbers.has(postNumber) || !Number.isFinite(version.observedAt) || version.observedAt < 0 || !validSource(version.source))
	      throw new Error(`Topic ${topicId} 的回复树删除版本无效`);
	    removedPostNumbers.add(postNumber);
	  }
	  return value;
	}
	class ReplyTreeRepository {
	  topicId;
	  topology;
	  changes = new import_signal.Signal();
	  #store;
	  #onPersistenceError;
	  #now;
	  #versions = /* @__PURE__ */ new Map();
	  #removedVersions = /* @__PURE__ */ new Map();
	  #expectedPostCount = 0;
	  #pendingSnapshot = null;
	  #persisting = null;
	  #lastPersistenceError = null;
	  #lastImplicitObservedAt = -1;
	  constructor(topicId, store, options = {}) {
	    const normalizedTopicId = String((0, import_identifiers.discourseTopicId)(topicId));
	    this.topicId = normalizedTopicId, this.#store = store, this.topology = options.topology ?? new import_reply_tree.ReplyTreeTopology(), this.#now = options.now ?? Date.now, this.#onPersistenceError = options.onPersistenceError ?? (() => {
	    });
	    for (const relation of this.topology.snapshot().relations)
	      this.#versions.set(relation.postNumber, Object.freeze({
	        postNumber: relation.postNumber,
	        observedAt: 0,
	        source: "loader-batch"
	      }));
	  }
	  coverage() {
	    const knownPostCount = this.topology.snapshot().relations.length;
	    return Object.freeze({
	      knownPostCount,
	      expectedPostCount: this.#expectedPostCount,
	      complete: this.#expectedPostCount > 0 && knownPostCount >= this.#expectedPostCount
	    });
	  }
	  setExpectedPostCount(expectedPostCount) {
	    const normalized = assertExpectedPostCount(expectedPostCount);
	    return normalized !== this.#expectedPostCount && (this.#expectedPostCount = normalized, this.#queuePersistence()), this.coverage();
	  }
	  ingest(posts, source, options = {}) {
	    const observedAt = options.observedAt === void 0 ? Math.max(
	      (0, import_ingest_version.discourseObservedAt)(this.#now()),
	      this.#lastImplicitObservedAt + 1
	    ) : (0, import_ingest_version.discourseObservedAt)(options.observedAt);
	    options.observedAt === void 0 && (this.#lastImplicitObservedAt = observedAt);
	    const relationByPost = /* @__PURE__ */ new Map(), acceptedByPost = /* @__PURE__ */ new Map();
	    let ignored = 0;
	    for (const post of posts) {
	      const relation = relationFromPost(post);
	      relation ? (relationByPost.delete(relation.postNumber), relationByPost.set(relation.postNumber, relation), acceptedByPost.delete(relation.postNumber), acceptedByPost.set(
	        relation.postNumber,
	        Object.freeze({ postNumber: relation.postNumber, post })
	      )) : ignored += 1;
	    }
	    const relations = [...relationByPost.values()], acceptedPosts = [...acceptedByPost.values()];
	    if (!relations.length)
	      return Object.freeze({
	        accepted: 0,
	        appliedRelations: 0,
	        acceptedPosts: Object.freeze([]),
	        ignored,
	        event: null,
	        listenerErrors: Object.freeze([])
	      });
	    const applicable = relations.filter((relation) => {
	      const removed = this.#removedVersions.get(relation.postNumber);
	      return removed && !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, { observedAt, source }) ? !1 : shouldApplyRelation(this.#versions.get(relation.postNumber), observedAt, source);
	    }), applicablePostNumbers = new Set(
	      applicable.map((relation) => relation.postNumber)
	    ), applicablePosts = acceptedPosts.filter((entry) => applicablePostNumbers.has(entry.postNumber));
	    if (!applicable.length)
	      return Object.freeze({
	        accepted: 0,
	        appliedRelations: 0,
	        acceptedPosts: Object.freeze([]),
	        ignored,
	        event: null,
	        listenerErrors: Object.freeze([])
	      });
	    const change = this.topology.commit(applicable);
	    for (const relation of applicable)
	      this.#removedVersions.delete(relation.postNumber), this.#versions.set(relation.postNumber, Object.freeze({
	        postNumber: relation.postNumber,
	        observedAt,
	        source
	      }));
	    const event = this.#event(source, change), listenerErrors = this.changes.emit(event);
	    return this.#queuePersistence(), Object.freeze({
	      accepted: applicable.length,
	      appliedRelations: applicable.length,
	      acceptedPosts: Object.freeze(applicablePosts),
	      ignored,
	      event,
	      listenerErrors
	    });
	  }
	  remove(rawPostNumber, source, options = {}) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawPostNumber);
	    if (postNumber === null) throw new RangeError("postNumber 必须是正安全整数");
	    const observedAt = options.observedAt === void 0 ? Math.max(
	      (0, import_ingest_version.discourseObservedAt)(this.#now()),
	      this.#lastImplicitObservedAt + 1
	    ) : (0, import_ingest_version.discourseObservedAt)(options.observedAt);
	    options.observedAt === void 0 && (this.#lastImplicitObservedAt = observedAt);
	    const current = this.#versions.get(postNumber), removed = this.#removedVersions.get(postNumber), nextVersion = Object.freeze({ postNumber, observedAt, source });
	    if (current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, nextVersion) || removed && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(removed, nextVersion))
	      return null;
	    const hadRelation = this.topology.has(postNumber), directChildren = this.topology.childrenOf(postNumber), change = this.topology.remove(postNumber);
	    this.#versions.delete(postNumber), this.#removedVersions.set(postNumber, nextVersion);
	    for (const childPostNumber of directChildren)
	      this.#versions.set(childPostNumber, Object.freeze({
	        postNumber: childPostNumber,
	        observedAt,
	        source
	      }));
	    (current || hadRelation) && (this.#expectedPostCount = Math.max(0, this.#expectedPostCount - 1));
	    const event = this.#event(source, change), listenerErrors = this.changes.emit(event);
	    for (const error of listenerErrors) this.#onPersistenceError(error);
	    return this.#queuePersistence(), event;
	  }
	  async restore() {
	    const revisionBeforeLoad = this.topology.revision;
	    let stored;
	    try {
	      stored = assertStoredSnapshot(await this.#store.load(this.topicId), this.topicId);
	    } catch (error) {
	      return this.#onPersistenceError(error), null;
	    }
	    if (!stored) return null;
	    const previousExpectedPostCount = this.#expectedPostCount, versionByPost = new Map(
	      stored.versions.map((version) => [version.postNumber, version])
	    );
	    let change, restoredRelations;
	    try {
	      if (this.topology.revision === revisionBeforeLoad && this.topology.snapshot().relations.length === 0)
	        change = this.topology.replace(stored.tree), restoredRelations = stored.tree.relations;
	      else {
	        const missingRelations = stored.tree.relations.filter(
	          (relation) => {
	            if (this.topology.has(relation.postNumber)) return !1;
	            const removed = this.#removedVersions.get(relation.postNumber), version = versionByPost.get(relation.postNumber);
	            return !removed || !version || (0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, version);
	          }
	        );
	        change = this.topology.commit(missingRelations), restoredRelations = missingRelations;
	      }
	    } catch (error) {
	      return this.#onPersistenceError(error), null;
	    }
	    for (const relation of restoredRelations) {
	      const version = versionByPost.get(relation.postNumber);
	      version && this.#versions.set(relation.postNumber, version);
	    }
	    const removalChanged = /* @__PURE__ */ new Set(), removalDetached = /* @__PURE__ */ new Set();
	    for (const removedVersion of stored.removedVersions ?? []) {
	      const current = this.#versions.get(removedVersion.postNumber), currentRemoval = this.#removedVersions.get(removedVersion.postNumber);
	      if (current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, removedVersion) || currentRemoval && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(currentRemoval, removedVersion))
	        continue;
	      const directChildren = this.topology.childrenOf(removedVersion.postNumber), removalChange = this.topology.remove(removedVersion.postNumber);
	      for (const postNumber of removalChange.changedPostNumbers)
	        removalChanged.add(postNumber);
	      for (const postNumber of removalChange.detachedPostNumbers)
	        removalDetached.add(postNumber);
	      this.#versions.delete(removedVersion.postNumber), this.#removedVersions.set(removedVersion.postNumber, removedVersion);
	      for (const childPostNumber of directChildren)
	        this.#versions.set(childPostNumber, Object.freeze({
	          postNumber: childPostNumber,
	          observedAt: removedVersion.observedAt,
	          source: removedVersion.source
	        }));
	    }
	    removalChanged.size && (change = Object.freeze({
	      revision: this.topology.revision,
	      changedPostNumbers: Object.freeze(
	        [.../* @__PURE__ */ new Set([
	          ...change.changedPostNumbers,
	          ...removalChanged
	        ])].sort((left, right) => left - right)
	      ),
	      detachedPostNumbers: Object.freeze(
	        [.../* @__PURE__ */ new Set([
	          ...change.detachedPostNumbers,
	          ...removalDetached
	        ])].sort((left, right) => left - right)
	      )
	    }));
	    const blockedStoredCount = stored.tree.relations.filter((relation) => {
	      const removed = this.#removedVersions.get(relation.postNumber), version = versionByPost.get(relation.postNumber);
	      return !!removed && !!version && !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, version);
	    }).length;
	    this.#expectedPostCount = Math.max(
	      this.#expectedPostCount,
	      Math.max(0, stored.expectedPostCount - blockedStoredCount)
	    );
	    const event = this.#event("cache-snapshot", change);
	    return this.changes.emit(event), (change.changedPostNumbers.length || this.#expectedPostCount !== previousExpectedPostCount) && this.#queuePersistence(), event;
	  }
	  snapshot(now = this.#now()) {
	    return Object.freeze({
	      schemaVersion: 2,
	      topicId: this.topicId,
	      savedAt: now,
	      expectedPostCount: this.#expectedPostCount,
	      tree: this.topology.snapshot(),
	      versions: Object.freeze(
	        [...this.#versions.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      removedVersions: Object.freeze(
	        [...this.#removedVersions.values()].sort((left, right) => left.postNumber - right.postNumber)
	      )
	    });
	  }
	  async flush() {
	    for (; this.#pendingSnapshot || this.#persisting; )
	      this.#persisting || this.#startPersistence(), await this.#persisting;
	    if (this.#lastPersistenceError !== null) {
	      const error = this.#lastPersistenceError;
	      throw this.#lastPersistenceError = null, error;
	    }
	  }
	  #event(source, change) {
	    return Object.freeze({
	      topicId: this.topicId,
	      source,
	      change,
	      coverage: this.coverage()
	    });
	  }
	  #queuePersistence() {
	    this.#pendingSnapshot = this.snapshot(), this.#persisting || this.#startPersistence();
	  }
	  #startPersistence() {
	    this.#persisting = Promise.resolve().then(async () => {
	      for (; this.#pendingSnapshot; ) {
	        const snapshot = this.#pendingSnapshot;
	        this.#pendingSnapshot = null;
	        try {
	          await this.#store.save(this.topicId, snapshot), this.#lastPersistenceError = null;
	        } catch (error) {
	          this.#lastPersistenceError = error, this.#onPersistenceError(error);
	        }
	      }
	    }).finally(() => {
	      this.#persisting = null, this.#pendingSnapshot && this.#startPersistence();
	    });
	  }
	}
}, "23a6e27565f0b410969e9712da2c67fea0bfd06ccbdc7b01940a2aa4ad5dffb0");

/* Source: lite/src/dom/reply-tree.ts */
runtime.register("src/dom/reply-tree.js", function(module, exports, require) {
	var reply_tree_exports = {};
	__export(reply_tree_exports, {
	  ReplyTreeTopology: () => ReplyTreeTopology
	});
	module.exports = __toCommonJS(reply_tree_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function assertPostNumber(value, field) {
	  try {
	    (0, import_identifiers.discoursePostNumber)(value);
	  } catch {
	    throw new RangeError(`${field} 必须是正安全整数`);
	  }
	}
	function sorted(values) {
	  return [...values].sort((left, right) => left - right);
	}
	const EMPTY_POST_NUMBERS = Object.freeze([]);
	class ReplyTreeTopology {
	  #revision = 0;
	  #parentByPost = /* @__PURE__ */ new Map();
	  #childrenByParent = /* @__PURE__ */ new Map();
	  #subtreePostCountByPost = /* @__PURE__ */ new Map();
	  #rootPostNumbers = /* @__PURE__ */ new Set();
	  #sortedChildrenByParent = /* @__PURE__ */ new Map();
	  #depthByPost = /* @__PURE__ */ new Map();
	  #rootByPost = /* @__PURE__ */ new Map();
	  #postNumbersCache = null;
	  #rootsCache = null;
	  #rootBranchesCache = null;
	  #snapshotCache = null;
	  get revision() {
	    return this.#revision;
	  }
	  parentOf(postNumber) {
	    return assertPostNumber(postNumber, "postNumber"), this.#parentByPost.get(postNumber);
	  }
	  childrenOf(parentPostNumber) {
	    assertPostNumber(parentPostNumber, "parentPostNumber");
	    const cached = this.#sortedChildrenByParent.get(parentPostNumber);
	    if (cached) return cached;
	    const children = this.#childrenByParent.get(parentPostNumber);
	    if (!children?.size) return EMPTY_POST_NUMBERS;
	    const result = Object.freeze(sorted(children));
	    return this.#sortedChildrenByParent.set(parentPostNumber, result), result;
	  }
	  postNumbers() {
	    return this.#postNumbersCache ? this.#postNumbersCache : (this.#postNumbersCache = Object.freeze(sorted(this.#parentByPost.keys())), this.#postNumbersCache);
	  }
	  roots() {
	    return this.#rootsCache ? this.#rootsCache : (this.#rootsCache = Object.freeze(sorted(this.#rootPostNumbers)), this.#rootsCache);
	  }
	  rootBranches() {
	    return this.#rootBranchesCache ? this.#rootBranchesCache : (this.#rootBranchesCache = Object.freeze(
	      this.roots().map((postNumber) => Object.freeze({
	        postNumber,
	        subtreePostCount: this.#subtreePostCountByPost.get(postNumber) ?? 1
	      }))
	    ), this.#rootBranchesCache);
	  }
	  clone() {
	    const clone = new ReplyTreeTopology();
	    return clone.#revision = this.#revision, clone.#parentByPost = new Map(this.#parentByPost), clone.#childrenByParent = new Map(
	      [...this.#childrenByParent].map(([postNumber, children]) => [
	        postNumber,
	        new Set(children)
	      ])
	    ), clone.#subtreePostCountByPost = new Map(
	      this.#subtreePostCountByPost
	    ), clone.#rootPostNumbers = new Set(this.#rootPostNumbers), clone.#sortedChildrenByParent = new Map(this.#sortedChildrenByParent), clone.#depthByPost = new Map(this.#depthByPost), clone.#rootByPost = new Map(this.#rootByPost), clone.#postNumbersCache = this.#postNumbersCache, clone.#rootsCache = this.#rootsCache, clone.#rootBranchesCache = this.#rootBranchesCache, clone.#snapshotCache = this.#snapshotCache, clone;
	  }
	  subtreePostCountOf(postNumber) {
	    return assertPostNumber(postNumber, "postNumber"), this.#subtreePostCountByPost.get(postNumber);
	  }
	  has(postNumber) {
	    return assertPostNumber(postNumber, "postNumber"), this.#parentByPost.has(postNumber);
	  }
	  depthOf(postNumber) {
	    if (assertPostNumber(postNumber, "postNumber"), !!this.#parentByPost.has(postNumber))
	      return this.#resolveAncestry(postNumber), this.#depthByPost.get(postNumber);
	  }
	  rootOf(postNumber) {
	    if (assertPostNumber(postNumber, "postNumber"), !!this.#parentByPost.has(postNumber))
	      return this.#resolveAncestry(postNumber), this.#rootByPost.get(postNumber);
	  }
	  commit(relations) {
	    if (!relations.length)
	      return Object.freeze({
	        revision: this.#revision,
	        changedPostNumbers: Object.freeze([]),
	        detachedPostNumbers: Object.freeze([])
	      });
	    const updates = /* @__PURE__ */ new Map(), touched = /* @__PURE__ */ new Set();
	    for (const relation of relations) {
	      if (assertPostNumber(relation.postNumber, "postNumber"), relation.parentPostNumber !== null && (assertPostNumber(relation.parentPostNumber, "parentPostNumber"), relation.parentPostNumber === relation.postNumber))
	        throw new Error(`楼层 #${relation.postNumber} 不能回复自身`);
	      updates.set(relation.postNumber, relation.parentPostNumber), touched.add(relation.postNumber);
	    }
	    this.#assertAcyclicWithUpdates(updates, touched);
	    const changed = /* @__PURE__ */ new Set(), detached = /* @__PURE__ */ new Set();
	    for (const postNumber of touched) {
	      const previousParent = this.#parentByPost.get(postNumber), nextParent = updates.get(postNumber);
	      previousParent === nextParent && this.#parentByPost.has(postNumber) || (changed.add(postNumber), previousParent != null && detached.add(postNumber));
	    }
	    if (!changed.size)
	      return Object.freeze({
	        revision: this.#revision,
	        changedPostNumbers: Object.freeze([]),
	        detachedPostNumbers: Object.freeze([])
	      });
	    let membershipChanged = !1, rootsChanged = !1;
	    const addedPostCount = [...changed].filter(
	      (postNumber) => !this.#parentByPost.has(postNumber)
	    ).length, projectedSize = this.#parentByPost.size + addedPostCount;
	    if (changed.size > 64 && changed.size * 4 >= projectedSize) {
	      const nextParents = this.#parentByPost.size === 0 ? updates : new Map(this.#parentByPost);
	      if (this.#parentByPost.size === 0)
	        membershipChanged = !0;
	      else
	        for (const postNumber of changed)
	          nextParents.has(postNumber) || (membershipChanged = !0), nextParents.set(postNumber, updates.get(postNumber));
	      this.#parentByPost = nextParents, this.#childrenByParent = this.#buildChildren(nextParents), this.#subtreePostCountByPost = this.#buildSubtreePostCounts(
	        nextParents,
	        this.#childrenByParent
	      ), this.#rootPostNumbers = /* @__PURE__ */ new Set();
	      for (const [postNumber, parentPostNumber] of nextParents)
	        parentPostNumber === null && this.#rootPostNumbers.add(postNumber);
	      this.#sortedChildrenByParent.clear(), rootsChanged = !0;
	    } else {
	      const affectedCounts = /* @__PURE__ */ new Set();
	      for (const postNumber of changed)
	        this.#addKnownAncestorChain(postNumber, affectedCounts);
	      for (const postNumber of changed) {
	        const hadPost = this.#parentByPost.has(postNumber), previousParent = this.#parentByPost.get(postNumber), nextParent = updates.get(postNumber);
	        hadPost || (membershipChanged = !0), hadPost && previousParent === null && (this.#rootPostNumbers.delete(postNumber), rootsChanged = !0), previousParent != null && this.#detachChild(previousParent, postNumber), this.#parentByPost.set(postNumber, nextParent), nextParent === null ? (this.#rootPostNumbers.add(postNumber), rootsChanged = !0) : this.#attachChild(nextParent, postNumber);
	      }
	      for (const postNumber of changed)
	        this.#addKnownAncestorChain(postNumber, affectedCounts);
	      this.#recomputeSubtreePostCounts(affectedCounts);
	    }
	    return this.#revision += 1, this.#depthByPost = /* @__PURE__ */ new Map(), this.#rootByPost = /* @__PURE__ */ new Map(), membershipChanged && (this.#postNumbersCache = null), rootsChanged && (this.#rootsCache = null), this.#rootBranchesCache = null, this.#snapshotCache = null, Object.freeze({
	      revision: this.#revision,
	      changedPostNumbers: Object.freeze(sorted(changed)),
	      detachedPostNumbers: Object.freeze(sorted(detached))
	    });
	  }
	  /**
	   * 删除一个关系,并把直属子楼层提升到被删楼层原父级。
	   *
	   * 这避免子孙因父 DOM 消失而变成无根悬挂节点;返回的 changed/detached 同时包含被删
	   * 楼层与需要重新挂载的直属子楼层。
	   */
	  remove(postNumber) {
	    if (assertPostNumber(postNumber, "postNumber"), !this.#parentByPost.has(postNumber))
	      return Object.freeze({
	        revision: this.#revision,
	        changedPostNumbers: Object.freeze([]),
	        detachedPostNumbers: Object.freeze([])
	      });
	    const previousParent = this.#parentByPost.get(postNumber) ?? null, directChildren = this.childrenOf(postNumber), affectedCounts = /* @__PURE__ */ new Set();
	    this.#addKnownAncestorChain(postNumber, affectedCounts), previousParent === null ? this.#rootPostNumbers.delete(postNumber) : this.#detachChild(previousParent, postNumber), this.#parentByPost.delete(postNumber), this.#childrenByParent.delete(postNumber), this.#sortedChildrenByParent.delete(postNumber), this.#subtreePostCountByPost.delete(postNumber);
	    for (const childPostNumber of directChildren)
	      this.#parentByPost.set(childPostNumber, previousParent), previousParent === null ? this.#rootPostNumbers.add(childPostNumber) : this.#attachChild(previousParent, childPostNumber), this.#addKnownAncestorChain(childPostNumber, affectedCounts);
	    return this.#recomputeSubtreePostCounts(affectedCounts), this.#revision += 1, this.#depthByPost = /* @__PURE__ */ new Map(), this.#rootByPost = /* @__PURE__ */ new Map(), this.#postNumbersCache = null, this.#rootsCache = null, this.#rootBranchesCache = null, this.#snapshotCache = null, Object.freeze({
	      revision: this.#revision,
	      changedPostNumbers: Object.freeze(sorted([postNumber, ...directChildren])),
	      detachedPostNumbers: Object.freeze(sorted([postNumber, ...directChildren]))
	    });
	  }
	  replace(snapshot) {
	    if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0)
	      throw new RangeError("回复树快照 revision 必须是非负安全整数");
	    const nextParents = /* @__PURE__ */ new Map();
	    for (const relation of snapshot.relations) {
	      if (assertPostNumber(relation.postNumber, "postNumber"), nextParents.has(relation.postNumber))
	        throw new Error(`快照重复定义楼层 #${relation.postNumber}`);
	      relation.parentPostNumber !== null && assertPostNumber(relation.parentPostNumber, "parentPostNumber"), nextParents.set(relation.postNumber, relation.parentPostNumber);
	    }
	    this.#assertAcyclic(nextParents, new Set(nextParents.keys()));
	    const changed = /* @__PURE__ */ new Set(), detached = /* @__PURE__ */ new Set();
	    for (const postNumber of /* @__PURE__ */ new Set([...this.#parentByPost.keys(), ...nextParents.keys()])) {
	      const previousParent = this.#parentByPost.get(postNumber), nextParent = nextParents.get(postNumber);
	      previousParent === nextParent && this.#parentByPost.has(postNumber) === nextParents.has(postNumber) || (changed.add(postNumber), nextParents.has(postNumber) || detached.add(postNumber));
	    }
	    return this.#parentByPost = nextParents, this.#childrenByParent = this.#buildChildren(nextParents), this.#subtreePostCountByPost = this.#buildSubtreePostCounts(
	      nextParents,
	      this.#childrenByParent
	    ), this.#rootPostNumbers = new Set(
	      [...nextParents].filter(([, parentPostNumber]) => parentPostNumber === null).map(([postNumber]) => postNumber)
	    ), this.#revision = Math.max(this.#revision + 1, snapshot.revision), this.#sortedChildrenByParent.clear(), this.#depthByPost = /* @__PURE__ */ new Map(), this.#rootByPost = /* @__PURE__ */ new Map(), this.#postNumbersCache = null, this.#rootsCache = null, this.#rootBranchesCache = null, this.#snapshotCache = null, Object.freeze({
	      revision: this.#revision,
	      changedPostNumbers: Object.freeze(sorted(changed)),
	      detachedPostNumbers: Object.freeze(sorted(detached))
	    });
	  }
	  snapshot() {
	    if (this.#snapshotCache) return this.#snapshotCache;
	    const relations = this.postNumbers().map(
	      (postNumber) => Object.freeze({
	        postNumber,
	        parentPostNumber: this.#parentByPost.get(postNumber) ?? null
	      })
	    );
	    return this.#snapshotCache = Object.freeze({
	      revision: this.#revision,
	      relations: Object.freeze(relations)
	    }), this.#snapshotCache;
	  }
	  #resolveAncestry(postNumber) {
	    if (this.#depthByPost.has(postNumber)) return;
	    const path = [];
	    let current = postNumber, depth, rootPostNumber;
	    for (; ; ) {
	      if (this.#depthByPost.has(current)) {
	        depth = this.#depthByPost.get(current), rootPostNumber = this.#rootByPost.get(current);
	        break;
	      }
	      if (!this.#parentByPost.has(current)) {
	        depth = void 0, rootPostNumber = void 0;
	        break;
	      }
	      const parentPostNumber = this.#parentByPost.get(current);
	      if (parentPostNumber === null) {
	        depth = 0, rootPostNumber = current, this.#depthByPost.set(current, depth), this.#rootByPost.set(current, rootPostNumber);
	        break;
	      }
	      path.push(current), current = parentPostNumber;
	    }
	    for (; path.length; ) {
	      const descendantPostNumber = path.pop();
	      depth !== void 0 && (depth += 1), this.#depthByPost.set(descendantPostNumber, depth), this.#rootByPost.set(descendantPostNumber, rootPostNumber);
	    }
	  }
	  #attachChild(parentPostNumber, postNumber) {
	    const children = this.#childrenByParent.get(parentPostNumber) ?? /* @__PURE__ */ new Set();
	    children.add(postNumber), this.#childrenByParent.set(parentPostNumber, children), this.#sortedChildrenByParent.delete(parentPostNumber);
	  }
	  #detachChild(parentPostNumber, postNumber) {
	    const children = this.#childrenByParent.get(parentPostNumber);
	    children && (children.delete(postNumber), children.size || this.#childrenByParent.delete(parentPostNumber), this.#sortedChildrenByParent.delete(parentPostNumber));
	  }
	  #addKnownAncestorChain(postNumber, target) {
	    const seen = /* @__PURE__ */ new Set();
	    let current = postNumber;
	    for (; current != null && this.#parentByPost.has(current) && !seen.has(current); )
	      seen.add(current), target.add(current), current = this.#parentByPost.get(current);
	  }
	  #knownDepth(postNumber, depthByPost) {
	    const cached = depthByPost.get(postNumber);
	    if (cached !== void 0) return cached;
	    const path = [];
	    let current = postNumber, depth = -1;
	    for (; ; ) {
	      const currentDepth = depthByPost.get(current);
	      if (currentDepth !== void 0) {
	        depth = currentDepth;
	        break;
	      }
	      if (!this.#parentByPost.has(current)) break;
	      path.push(current);
	      const parentPostNumber = this.#parentByPost.get(current);
	      if (parentPostNumber == null || !this.#parentByPost.has(parentPostNumber)) break;
	      current = parentPostNumber;
	    }
	    for (; path.length; )
	      depth += 1, depthByPost.set(path.pop(), depth);
	    return depthByPost.get(postNumber) ?? 0;
	  }
	  #recomputeSubtreePostCounts(affected) {
	    const depthByPost = /* @__PURE__ */ new Map(), postNumbers = [...affected].filter((postNumber) => this.#parentByPost.has(postNumber)).sort(
	      (left, right) => this.#knownDepth(right, depthByPost) - this.#knownDepth(left, depthByPost) || right - left
	    );
	    for (const postNumber of postNumbers) {
	      let subtreePostCount = 1;
	      for (const childPostNumber of this.#childrenByParent.get(postNumber) ?? [])
	        subtreePostCount += this.#subtreePostCountByPost.get(childPostNumber) ?? 1;
	      this.#subtreePostCountByPost.set(postNumber, subtreePostCount);
	    }
	  }
	  #buildChildren(parents) {
	    const children = /* @__PURE__ */ new Map();
	    for (const [postNumber, parentPostNumber] of parents) {
	      if (parentPostNumber === null) continue;
	      const siblings = children.get(parentPostNumber) ?? /* @__PURE__ */ new Set();
	      siblings.add(postNumber), children.set(parentPostNumber, siblings);
	    }
	    return children;
	  }
	  #buildSubtreePostCounts(parents, children) {
	    const counts = /* @__PURE__ */ new Map(), remainingChildren = /* @__PURE__ */ new Map(), queue = [];
	    for (const postNumber of parents.keys()) {
	      const count = children.get(postNumber)?.size ?? 0;
	      remainingChildren.set(postNumber, count), count === 0 && queue.push(postNumber);
	    }
	    for (; queue.length; ) {
	      const postNumber = queue.pop(), subtreePostCount = counts.get(postNumber) ?? 1;
	      counts.set(postNumber, subtreePostCount);
	      const parentPostNumber = parents.get(postNumber);
	      if (parentPostNumber == null || !parents.has(parentPostNumber)) continue;
	      counts.set(
	        parentPostNumber,
	        (counts.get(parentPostNumber) ?? 1) + subtreePostCount
	      );
	      const remaining = (remainingChildren.get(parentPostNumber) ?? 1) - 1;
	      remainingChildren.set(parentPostNumber, remaining), remaining === 0 && queue.push(parentPostNumber);
	    }
	    return counts;
	  }
	  #assertAcyclic(parents, starts) {
	    this.#assertAcyclicFrom(
	      (postNumber) => parents.get(postNumber),
	      starts
	    );
	  }
	  #assertAcyclicWithUpdates(updates, starts) {
	    this.#assertAcyclicFrom(
	      (postNumber) => updates.has(postNumber) ? updates.get(postNumber) : this.#parentByPost.get(postNumber),
	      starts
	    );
	  }
	  #assertAcyclicFrom(parentOf, starts) {
	    const complete = /* @__PURE__ */ new Set();
	    for (const start of starts) {
	      if (complete.has(start)) continue;
	      const path = /* @__PURE__ */ new Set(), traversed = [];
	      let current = start;
	      for (; current != null && !complete.has(current); ) {
	        if (path.has(current))
	          throw new Error(`楼层关系存在环,经过 #${current}`);
	        path.add(current), traversed.push(current), current = parentOf(current);
	      }
	      for (const postNumber of traversed) complete.add(postNumber);
	    }
	  }
	}
}, "cddb81ad8170f21ba3e8274c9e7c28173b0f74f8a7084202bb52d62bf8fd63d9");

/* Source: lite/src/dom/required-element.ts */
runtime.register("src/dom/required-element.js", function(module, exports, require) {
	var required_element_exports = {};
	__export(required_element_exports, {
	  requiredElementQuery: () => requiredElementQuery
	});
	module.exports = __toCommonJS(required_element_exports);
	function requiredElementQuery(owner) {
	  return function(root, selector) {
	    const node = root.querySelector(selector);
	    if (!node) throw new Error(`${owner}缺少 ${selector}`);
	    return node;
	  };
	}
}, "7f4374644d667871f13c79db93a71b41820a21560f1fddeeb6c081f26c783f8e");

/* Source: lite/src/kernel/lifecycle.ts */
runtime.register("src/kernel/lifecycle.js", function(module, exports, require) {
	var lifecycle_exports = {};
	__export(lifecycle_exports, {
	  LifecycleScope: () => LifecycleScope
	});
	module.exports = __toCommonJS(lifecycle_exports);
	class LifecycleScope {
	  #cleanups = [];
	  #destroyed = !1;
	  /**
	   * 为业务 owner 创建唯一作用域:有父级时建立受控 child,否则建立独立 root。
	   */
	  static ownedBy(parent) {
	    return parent ? parent.child() : new LifecycleScope();
	  }
	  get destroyed() {
	    return this.#destroyed;
	  }
	  add(cleanup) {
	    let active = !0;
	    const runOnce = () => {
	      if (!active) return;
	      active = !1;
	      const index = this.#cleanups.indexOf(runOnce);
	      index >= 0 && this.#cleanups.splice(index, 1), cleanup();
	    };
	    return this.#destroyed ? (runOnce(), runOnce) : (this.#cleanups.push(runOnce), runOnce);
	  }
	  child() {
	    const child = new LifecycleScope(), detach = this.add(() => child.destroy());
	    return child.add(detach), child;
	  }
	  /** 创建由本 scope 独占、可选转发上游取消原因的 AbortController。 */
	  abortController(destroyReason, upstream) {
	    const controller = new AbortController(), abort = (reason) => {
	      controller.signal.aborted || controller.abort(reason);
	    }, forwardAbort = () => abort(upstream?.reason);
	    return upstream?.aborted ? forwardAbort() : upstream?.addEventListener("abort", forwardAbort, { once: !0 }), this.add(() => {
	      upstream?.removeEventListener("abort", forwardAbort), abort(destroyReason);
	    }), controller;
	  }
	  listen(target, type, listener, options) {
	    return target.addEventListener(type, listener, options), this.add(() => target.removeEventListener(type, listener, options));
	  }
	  observe(observer, target, options) {
	    return target && "observe" in observer && typeof observer.observe == "function" && observer.observe(target, options), this.add(() => observer.disconnect());
	  }
	  timer(timerId, clear = clearTimeout) {
	    return this.add(() => clear(timerId));
	  }
	  destroy() {
	    if (this.#destroyed) return;
	    this.#destroyed = !0;
	    const errors = [], cleanups = this.#cleanups.splice(0);
	    for (let index = cleanups.length - 1; index >= 0; index -= 1)
	      try {
	        cleanups[index]();
	      } catch (error) {
	        errors.push(error);
	      }
	    if (errors.length) throw new AggregateError(errors, "LifecycleScope cleanup failed");
	  }
	}
}, "8b8b2ccae7437d794799c0a26fd4258ce04e126181396d2ee18e7dea48d2cd62");

/* Source: lite/src/kernel/repeat-action-gate.ts */
runtime.register("src/kernel/repeat-action-gate.js", function(module, exports, require) {
	var repeat_action_gate_exports = {};
	__export(repeat_action_gate_exports, {
	  RepeatActionGate: () => RepeatActionGate
	});
	module.exports = __toCommonJS(repeat_action_gate_exports);
	class RepeatActionGate {
	  #deadlines = /* @__PURE__ */ new Map();
	  #windowMs;
	  #now;
	  constructor(options = {}) {
	    const windowMs = Number(options.windowMs ?? 1500);
	    if (!Number.isFinite(windowMs) || windowMs <= 0)
	      throw new RangeError("重复动作确认期限必须为正数");
	    this.#windowMs = windowMs, this.#now = options.now ?? Date.now;
	  }
	  confirm(rawKey) {
	    const key = String(rawKey).trim();
	    if (!key) throw new TypeError("重复动作确认 key 不能为空");
	    const now = this.#now(), confirmed = (this.#deadlines.get(key) ?? 0) >= now;
	    return this.#deadlines.clear(), confirmed || this.#deadlines.set(key, now + this.#windowMs), confirmed;
	  }
	  clear() {
	    this.#deadlines.clear();
	  }
	}
}, "405ed67abebe6114e3bf920269d0e06acd24f7aa7089e329bd65a7eefffa467a");

/* Source: lite/src/kernel/signal.ts */
runtime.register("src/kernel/signal.js", function(module, exports, require) {
	var signal_exports = {};
	__export(signal_exports, {
	  Signal: () => Signal
	});
	module.exports = __toCommonJS(signal_exports);
	class Signal {
	  #listeners = /* @__PURE__ */ new Set();
	  get size() {
	    return this.#listeners.size;
	  }
	  subscribe(listener, scope) {
	    this.#listeners.add(listener);
	    const unsubscribe = () => {
	      this.#listeners.delete(listener);
	    };
	    return scope && scope.add(unsubscribe), unsubscribe;
	  }
	  emit(value) {
	    const errors = [];
	    for (const listener of [...this.#listeners])
	      try {
	        listener(value);
	      } catch (error) {
	        errors.push(error);
	      }
	    return Object.freeze(errors);
	  }
	  clear() {
	    this.#listeners.clear();
	  }
	}
}, "9e0cff36c2177073a516fade2a21fee65bb7ac09607649e2c719742a2c129ae4");

/* Source: lite/src/kernel/value-record.ts */
runtime.register("src/kernel/value-record.js", function(module, exports, require) {
	var value_record_exports = {};
	__export(value_record_exports, {
	  objectRecord: () => objectRecord,
	  valueRecord: () => valueRecord
	});
	module.exports = __toCommonJS(value_record_exports);
	function valueRecord(value) {
	  return value !== null && (typeof value == "object" || typeof value == "function") ? value : null;
	}
	function objectRecord(value) {
	  return value !== null && typeof value == "object" ? value : null;
	}
}, "e86289a6fd7f06e9a422bff290f94ee01c33fd53a37836fa46491175d6dd1356");

/* Source: lite/src/layout/branch-overlay.ts */
runtime.register("src/layout/branch-overlay.js", function(module, exports, require) {
	var branch_overlay_exports = {};
	__export(branch_overlay_exports, {
	  ReaderBranchOverlayController: () => ReaderBranchOverlayController,
	  deriveBranchGeometry: () => deriveBranchGeometry
	});
	module.exports = __toCommonJS(branch_overlay_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_reader_icon = require("../components/reader-icon.js");
	const CHILD_AVATAR_GAP = 5, PARENT_AVATAR_DROP_GAP = CHILD_AVATAR_GAP, MAX_CONTINUOUS_BRANCH_SPAN_PX = 2048, BRANCH_PAINT_PADDING_PX = 8;
	function finite(value, field) {
	  if (!Number.isFinite(value)) throw new RangeError(`${field} 必须是有限数值`);
	  return value;
	}
	function deriveBranchGeometry(input) {
	  const parentAxisX = finite(input.parentAxisX, "parentAxisX"), parentStartY = finite(input.parentStartY, "parentStartY"), childAxisX = finite(input.childAxisX, "childAxisX"), childCenterY = finite(input.childCenterY, "childCenterY"), requestedRadius = Math.max(0, finite(input.cornerRadius, "cornerRadius")), horizontalDistance = Math.abs(childAxisX - parentAxisX), verticalDistance = Math.abs(childCenterY - parentStartY), radius = Math.min(requestedRadius, horizontalDistance, verticalDistance), turnY = childCenterY - Math.sign(childCenterY - parentStartY || 1) * radius, turnX = parentAxisX + Math.sign(childAxisX - parentAxisX || 1) * radius, path = [
	    `M ${parentAxisX} ${parentStartY}`,
	    `L ${parentAxisX} ${turnY}`,
	    `Q ${parentAxisX} ${childCenterY} ${turnX} ${childCenterY}`,
	    `L ${childAxisX} ${childCenterY}`
	  ].join(" ");
	  return Object.freeze({
	    path
	  });
	}
	function deriveSharedBranchGeometry(input) {
	  const parentAxisX = finite(input.parentAxisX, "parentAxisX"), parentStartY = finite(input.parentStartY, "parentStartY"), requestedRadius = Math.max(
	    0,
	    finite(input.cornerRadius, "cornerRadius")
	  ), branches = input.children.map((child) => {
	    const childAxisX = finite(child.childAxisX, "childAxisX"), childCenterY = finite(child.childCenterY, "childCenterY"), horizontalDirection = Math.sign(childAxisX - parentAxisX || 1), verticalDirection = Math.sign(childCenterY - parentStartY || 1), radius = Math.min(
	      requestedRadius,
	      Math.abs(childAxisX - parentAxisX),
	      Math.abs(childCenterY - parentStartY)
	    );
	    return Object.freeze({
	      childAxisX,
	      childCenterY,
	      turnX: parentAxisX + horizontalDirection * radius,
	      turnY: childCenterY - verticalDirection * radius
	    });
	  });
	  if (!branches.length) return Object.freeze({ path: "" });
	  const railEnd = branches.reduce(
	    (farthest, branch) => Math.abs(branch.turnY - parentStartY) > Math.abs(farthest - parentStartY) ? branch.turnY : farthest,
	    branches[0].turnY
	  ), parts = [
	    `M ${parentAxisX} ${parentStartY}`,
	    `L ${parentAxisX} ${railEnd}`
	  ];
	  for (const branch of branches)
	    parts.push(
	      `M ${parentAxisX} ${branch.turnY}`,
	      `Q ${parentAxisX} ${branch.childCenterY} ${branch.turnX} ${branch.childCenterY}`,
	      `L ${branch.childAxisX} ${branch.childCenterY}`
	    );
	  return Object.freeze({ path: parts.join(" ") });
	}
	function branchScrollContainer(element) {
	  const view = element.ownerDocument.defaultView;
	  for (let candidate = element.parentElement; candidate; candidate = candidate.parentElement) {
	    const overflowY = view?.getComputedStyle(candidate).overflowY ?? "";
	    if (/^(auto|scroll|overlay)$/.test(overflowY) && candidate.scrollHeight > candidate.clientHeight) return candidate;
	  }
	  return null;
	}
	function pointerAnchor(event, root, wasCollapsed) {
	  const pointer = event;
	  return wasCollapsed || pointer.detail <= 0 || !Number.isFinite(pointer.clientY) ? null : Object.freeze({
	    clientY: pointer.clientY,
	    scrollContainer: branchScrollContainer(root)
	  });
	}
	function restorePointerAnchor(anchor, toggle) {
	  if (!anchor || !toggle?.isConnected) return;
	  const rect = toggle.getBoundingClientRect(), delta = rect.top + rect.height / 2 - anchor.clientY;
	  if (!(!Number.isFinite(delta) || Math.abs(delta) < 0.5)) {
	    if (anchor.scrollContainer?.isConnected) {
	      anchor.scrollContainer.scrollTop += delta;
	      return;
	    }
	    toggle.ownerDocument.defaultView?.scrollBy({
	      top: delta,
	      behavior: "auto"
	    });
	  }
	}
	function defaultReadAvatar(slots) {
	  const collapsedToggle = slots.root.querySelector(
	    ':scope > .ldp-reader-branch-toggle[aria-expanded="false"]'
	  );
	  return collapsedToggle || slots.header.querySelector(
	    "[data-reader-avatar],.ldp-avatar-link,.ldp-avatar,img.avatar"
	  );
	}
	function finiteRect(rect) {
	  return Number.isFinite(rect.left) && Number.isFinite(rect.right) && Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && Number.isFinite(rect.width) && Number.isFinite(rect.height);
	}
	function visiblePath(slots) {
	  return slots.branchOverlay.querySelector(
	    ".ldp-branch-visible-path"
	  );
	}
	function hitPath(slots) {
	  return slots.branchOverlay.querySelector(
	    ".ldp-branch-hit-path"
	  );
	}
	class ReaderBranchOverlayController {
	  scope;
	  #domOwner;
	  #renderMode;
	  #allowLongBranchSpans;
	  #readAvatar;
	  #onLayoutChange;
	  #onToggleBranch;
	  #readCollapsed;
	  #owned = /* @__PURE__ */ new Map();
	  #collapsed = /* @__PURE__ */ new Set();
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#domOwner = options.domOwner, this.#renderMode = options.renderMode ?? "svg", this.#allowLongBranchSpans = options.allowLongBranchSpans ?? !1, this.#readAvatar = options.readAvatar ?? defaultReadAvatar, this.#onLayoutChange = options.onLayoutChange ?? (() => {
	    }), this.#onToggleBranch = options.onToggleBranch ?? null, this.#readCollapsed = options.readCollapsed ?? null, this.#renderMode === "segmented-css" && (this.#domOwner.rootList.classList.add("ldp-segmented-branches"), this.scope.add(() => {
	      this.#domOwner.rootList.classList.remove("ldp-segmented-branches");
	    })), this.scope.listen(this.#domOwner.rootList, "click", (event) => {
	      const target = event.target;
	      if (!target || target.nodeType !== 1) return;
	      const element = target, toggle = element.closest(
	        "[data-reader-branch-toggle]"
	      );
	      if (toggle?.disabled) return;
	      const hit = element.closest(
	        ".ldp-branch-hit-path"
	      ), root = (toggle ?? hit)?.closest(
	        ".ldp-post[data-post-number]"
	      );
	      if (!root) return;
	      const postNumber = Number(
	        toggle?.dataset.readerBranchToggle ?? root.dataset.postNumber
	      );
	      if (!Number.isSafeInteger(postNumber) || postNumber <= 0) return;
	      const wasCollapsed = this.#isCollapsed(postNumber), anchor = !wasCollapsed && (options.preserveCollapseAnchor?.(root) ?? !1) ? null : pointerAnchor(event, root, wasCollapsed);
	      event.preventDefault(), event.stopPropagation(), this.toggle(postNumber), restorePointerAnchor(
	        anchor,
	        this.#owned.get(postNumber)?.toggle ?? null
	      );
	    });
	    const resizeObserver = options.createResizeObserver?.(() => {
	      if (!this.scope.destroyed) {
	        if (options.onObservedResize) {
	          options.onObservedResize();
	          return;
	        }
	        this.paint(), this.#onLayoutChange();
	      }
	    }) ?? null;
	    resizeObserver && (resizeObserver.observe(this.#domOwner.rootList), this.scope.add(() => resizeObserver.disconnect())), this.scope.add(() => this.#restore());
	  }
	  paint() {
	    if (this.scope.destroyed)
	      return Object.freeze({
	        paintedBranches: 0
	      });
	    if (this.#sweep(), this.#renderMode === "segmented-css")
	      return this.#paintSegmentedBranches();
	    const { measurements, invalidViews } = this.#measure();
	    let paintedBranches = 0;
	    for (const view of invalidViews) this.#clear(view);
	    for (const measurement of measurements) {
	      const { view } = measurement, slots = view.slots, hasChildren = measurement.hasChildren;
	      if (this.#setClass(slots.root, "ldp-has-child-branches", hasChildren), !hasChildren) {
	        this.#clear(view);
	        continue;
	      }
	      const owned = this.#own(view), projectedOverlayLeft = measurement.overlayRect.left, projectedOverlayWidth = Math.max(1, measurement.overlayRect.width);
	      owned.toggle.hidden === hasChildren && (owned.toggle.hidden = !hasChildren);
	      const parentAxisX = measurement.avatarRect.left + measurement.avatarRect.width / 2 - projectedOverlayLeft, toggleLeft = `${measurement.avatarRect.left + measurement.avatarRect.width / 2 - measurement.rootRect.left}px`;
	      owned.toggle.style.left !== toggleLeft && (owned.toggle.style.left = toggleLeft);
	      const toggleTop = `${measurement.toggleCenterY}px`;
	      if (owned.toggle.style.top !== toggleTop && (owned.toggle.style.top = toggleTop), this.#syncCollapsed(view, owned.toggle), this.#isCollapsed(view.postNumber)) {
	        this.#clearPaths(slots);
	        continue;
	      }
	      const parentStartY = measurement.avatarRect.bottom - measurement.overlayRect.top + PARENT_AVATAR_DROP_GAP;
	      if (!measurement.childAvatarRects.length) {
	        this.#clearPaths(slots);
	        continue;
	      }
	      const childGeometry = measurement.childAvatarRects.map(
	        (childAvatarRect) => Object.freeze({
	          childAxisX: childAvatarRect.left - measurement.overlayRect.left - CHILD_AVATAR_GAP,
	          childCenterY: childAvatarRect.top + childAvatarRect.height / 2 - measurement.overlayRect.top
	        })
	      ), geometry = deriveSharedBranchGeometry({
	        parentAxisX,
	        parentStartY,
	        children: childGeometry,
	        cornerRadius: measurement.cornerRadius
	      });
	      paintedBranches += measurement.childAvatarRects.length;
	      const path = geometry.path, visible = visiblePath(slots), hit = hitPath(slots);
	      slots.branchOverlay.hasAttribute("hidden") && slots.branchOverlay.removeAttribute("hidden"), this.#setAttribute(visible, "d", path), this.#setAttribute(hit, "d", path), this.#setStyle(
	        visible,
	        "stroke-width",
	        "var(--ldp-reply-line-width,1px)"
	      ), this.#setStyle(
	        hit,
	        "stroke-width",
	        "var(--ldp-reply-line-hit-width,8px)"
	      );
	      const paintBlockSize = Math.max(
	        1,
	        Math.ceil(
	          Math.max(
	            parentStartY,
	            ...childGeometry.map((child) => child.childCenterY)
	          ) + measurement.cornerRadius + BRANCH_PAINT_PADDING_PX
	        )
	      );
	      this.#setStyle(
	        slots.branchOverlay,
	        "height",
	        `${paintBlockSize}px`
	      ), this.#setAttribute(
	        slots.branchOverlay,
	        "viewBox",
	        `0 0 ${projectedOverlayWidth} ${paintBlockSize}`
	      );
	    }
	    return Object.freeze({ paintedBranches });
	  }
	  #paintSegmentedBranches() {
	    const views = this.#domOwner.views(), childrenByParent = /* @__PURE__ */ new Map();
	    for (const child of views) {
	      if (!child.slots.root.isConnected) continue;
	      const parentPostNumber = this.#domOwner.topology.parentOf(
	        child.postNumber
	      );
	      if (parentPostNumber == null) continue;
	      const children = childrenByParent.get(parentPostNumber) ?? [];
	      children.push(child), childrenByParent.set(parentPostNumber, children);
	    }
	    const lastChildByParent = /* @__PURE__ */ new Map();
	    for (const [parentPostNumber, mountedChildren] of childrenByParent) {
	      const lastChildPostNumber = this.#domOwner.topology.childrenOf?.(parentPostNumber)?.at(-1) ?? [...mountedChildren].sort((left, right) => left.postNumber - right.postNumber).at(-1)?.postNumber;
	      lastChildPostNumber !== void 0 && lastChildByParent.set(parentPostNumber, lastChildPostNumber);
	    }
	    const segmentedRootToggleTop = this.#measureSegmentedRootToggleTop(
	      views,
	      childrenByParent
	    );
	    let paintedBranches = 0;
	    for (const view of views) {
	      const parentPostNumber = this.#domOwner.topology.parentOf(
	        view.postNumber
	      );
	      this.#setClass(
	        view.slots.root,
	        "ldp-segmented-branch-last",
	        view.slots.root.isConnected && parentPostNumber !== null && parentPostNumber !== void 0 && lastChildByParent.get(parentPostNumber) === view.postNumber
	      );
	      const children = childrenByParent.get(view.postNumber) ?? [], hasChildren = view.slots.root.isConnected && children.length > 0;
	      if (this.#setClass(
	        view.slots.root,
	        "ldp-has-child-branches",
	        hasChildren
	      ), this.#clearPaths(view.slots), !hasChildren) {
	        this.#clear(view);
	        continue;
	      }
	      const owned = this.#own(view);
	      this.#syncSegmentedRailToggles(owned, children), owned.toggle.style.left && owned.toggle.style.removeProperty("left"), view.postNumber !== 1 && owned.toggle.style.top && owned.toggle.style.removeProperty("top"), view.postNumber === 1 && segmentedRootToggleTop !== null && this.#setStyle(
	        owned.toggle,
	        "top",
	        `${segmentedRootToggleTop}px`
	      ), owned.toggle.hidden && (owned.toggle.hidden = !1), this.#syncCollapsed(view, owned.toggle), !this.#isCollapsed(view.postNumber) && (paintedBranches += children.filter(
	        (child) => !child.slots.root.classList.contains("ldp-virtual-ancestor-shell")
	      ).length);
	    }
	    return Object.freeze({ paintedBranches });
	  }
	  #measureSegmentedRootToggleTop(views, childrenByParent) {
	    const rootView = views.find((view) => view.postNumber === 1);
	    if (!rootView?.slots.root.isConnected || this.#isCollapsed(rootView.postNumber)) return null;
	    const childByRoot = new Map(
	      (childrenByParent.get(rootView.postNumber) ?? []).map((child) => [
	        child.slots.root,
	        child
	      ])
	    );
	    let firstChild = null;
	    for (const element of rootView.slots.replyList.children) {
	      const child = childByRoot.get(element);
	      if (child?.slots.root.isConnected && !child.slots.root.classList.contains("ldp-virtual-ancestor-shell")) {
	        firstChild = child;
	        break;
	      }
	    }
	    if (!firstChild) return null;
	    const rootReplyTreeRect = rootView.slots.replyTree.getBoundingClientRect(), childRootRect = firstChild.slots.root.getBoundingClientRect(), childActionsRect = firstChild.slots.actions.getBoundingClientRect(), childReplyTreeRect = firstChild.slots.replyTree.getBoundingClientRect();
	    if (!finiteRect(rootReplyTreeRect) || !finiteRect(childRootRect) || !finiteRect(childActionsRect) || !finiteRect(childReplyTreeRect) || childActionsRect.height <= 0) return null;
	    const ordinaryToggleCenterY = childActionsRect.top + childActionsRect.height / 2, ordinaryTailDistance = childReplyTreeRect.top - ordinaryToggleCenterY;
	    if (!Number.isFinite(ordinaryTailDistance) || ordinaryTailDistance < 0)
	      return null;
	    const top = childRootRect.top - rootReplyTreeRect.top - ordinaryTailDistance;
	    return Number.isFinite(top) ? top : null;
	  }
	  toggle(postNumber) {
	    if (this.scope.destroyed) return;
	    const view = this.#domOwner.view(postNumber);
	    !view || !this.#hasChildren(postNumber) || (this.#onToggleBranch ? this.#onToggleBranch(postNumber) : this.#collapsed.has(postNumber) ? this.#collapsed.delete(postNumber) : this.#collapsed.add(postNumber), this.#syncCollapsed(view, this.#own(view).toggle), this.#onLayoutChange());
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  /**
	   * 释放当前树投影,但保留 controller 供同一 surface 下次重新挂载。
	   *
	   * 完整讨论关闭时不会再等待下一次 paint 才 sweep 已销毁的 PostView;主流切帖也可
	   * 用同一路径立即恢复接管前 margin/折叠/按钮状态并丢弃旧 View 引用。
	   */
	  releaseProjection() {
	    this.scope.destroyed || this.#restore();
	  }
	  #measure() {
	    const measurements = [], invalidViews = [], views = this.#domOwner.views(), childrenByParent = /* @__PURE__ */ new Map(), geometryByPost = /* @__PURE__ */ new Map(), avatarByPost = /* @__PURE__ */ new Map(), transientPostNumbers = /* @__PURE__ */ new Set(), invalidPostNumbers = /* @__PURE__ */ new Set(), styleGeometryByVariant = /* @__PURE__ */ new Map(), invalidate = (view) => {
	      invalidPostNumbers.add(view.postNumber), invalidViews.includes(view) || invalidViews.push(view);
	    };
	    for (const child of views) {
	      const parentPostNumber = this.#domOwner.topology.parentOf(
	        child.postNumber
	      );
	      if (parentPostNumber == null)
	        continue;
	      const children = childrenByParent.get(parentPostNumber) ?? [];
	      children.push(child), childrenByParent.set(parentPostNumber, children);
	    }
	    for (const view of views) {
	      const slots = view.slots;
	      if (!slots.root.isConnected) {
	        invalidate(view);
	        continue;
	      }
	      const avatar = this.#readAvatar(slots);
	      if (!avatar) {
	        invalidate(view);
	        continue;
	      }
	      const avatarRect = avatar.getBoundingClientRect(), rootRect = slots.root.getBoundingClientRect(), overlayRect = slots.replyTree.getBoundingClientRect();
	      if (!finiteRect(avatarRect) || !finiteRect(rootRect) || !finiteRect(overlayRect)) {
	        invalidate(view);
	        continue;
	      }
	      if (avatarRect.width > 0 && avatarRect.height > 0 ? avatarByPost.set(view.postNumber, avatarRect) : transientPostNumbers.add(view.postNumber), avatarRect.width <= 0 || avatarRect.height <= 0 || rootRect.width <= 0 || rootRect.height <= 0 || overlayRect.width <= 0 || overlayRect.height <= 0) {
	        transientPostNumbers.add(view.postNumber);
	        continue;
	      }
	      geometryByPost.set(view.postNumber, Object.freeze({
	        view,
	        avatarRect,
	        rootRect,
	        overlayRect
	      }));
	    }
	    for (const snapshot of geometryByPost.values()) {
	      const { view, avatarRect, rootRect, overlayRect } = snapshot, slots = view.slots, collapsed = this.#isCollapsed(view.postNumber), styleKey = this.#styleGeometryKey(slots.root);
	      let styleGeometry = styleGeometryByVariant.get(styleKey);
	      if (!styleGeometry) {
	        const rootStyle = slots.root.ownerDocument.defaultView?.getComputedStyle?.(slots.root);
	        styleGeometry = Object.freeze({
	          cornerRadius: this.#cornerRadius(rootStyle)
	        }), styleGeometryByVariant.set(styleKey, styleGeometry);
	      }
	      const childAvatarRects = [], actionsRect = slots.actions.getBoundingClientRect(), retainedToggleTop = Number.parseFloat(
	        this.#owned.get(view.postNumber)?.toggle.style.top ?? ""
	      );
	      let toggleCenterY = collapsed && Number.isFinite(retainedToggleTop) ? retainedToggleTop : finiteRect(actionsRect) && actionsRect.height > 0 ? actionsRect.top + actionsRect.height / 2 - rootRect.top : 0;
	      const children = [...childrenByParent.get(view.postNumber) ?? []].sort((left, right) => left.postNumber - right.postNumber);
	      if (!collapsed) {
	        const drawableChildren = children.filter(
	          (child) => child.slots.root.isConnected && !child.slots.root.classList.contains(
	            "ldp-virtual-ancestor-shell"
	          )
	        );
	        if (drawableChildren.filter(
	          (child) => !avatarByPost.has(child.postNumber)
	        ).some(
	          (child) => transientPostNumbers.has(child.postNumber) && !invalidPostNumbers.has(child.postNumber)
	        ))
	          continue;
	        for (const child of drawableChildren) {
	          const childAvatarRect = avatarByPost.get(child.postNumber);
	          childAvatarRect && (this.#allowLongBranchSpans || Math.abs(
	            childAvatarRect.top + childAvatarRect.height / 2 - (avatarRect.bottom + PARENT_AVATAR_DROP_GAP)
	          ) <= MAX_CONTINUOUS_BRANCH_SPAN_PX) && childAvatarRects.push(childAvatarRect);
	        }
	        childAvatarRects.sort((left, right) => left.top - right.top);
	      }
	      !collapsed && view.postNumber === 1 && childAvatarRects[0] && (toggleCenterY = childAvatarRects[0].top - rootRect.top - 10), measurements.push(Object.freeze({
	        view,
	        avatarRect,
	        rootRect,
	        overlayRect,
	        toggleCenterY,
	        hasChildren: children.length > 0,
	        childAvatarRects: Object.freeze(childAvatarRects),
	        cornerRadius: styleGeometry.cornerRadius
	      }));
	    }
	    return Object.freeze({
	      measurements: Object.freeze(measurements),
	      invalidViews: Object.freeze(invalidViews)
	    });
	  }
	  #own(view) {
	    const existing = this.#owned.get(view.postNumber);
	    if (existing?.view === view) return existing;
	    existing && this.#releaseOwned(existing);
	    const toggle = view.slots.root.ownerDocument.createElement("button");
	    toggle.type = "button", toggle.className = "ldp-collapse-replies ldp-reply-rail-control ldp-reader-branch-toggle show", toggle.dataset.readerBranchToggle = String(view.postNumber);
	    const count = view.slots.root.ownerDocument.createElement("span");
	    count.className = "ldp-collapsed-branch-count", count.hidden = !0, view.slots.replyControls.classList.add("ldp-branch-controls"), view.slots.root.insertBefore(toggle, view.slots.header), view.slots.header.after(count);
	    const trunkToggle = this.#renderMode === "segmented-css" ? view.slots.root.ownerDocument.createElement("button") : null;
	    trunkToggle && (trunkToggle.type = "button", trunkToggle.className = "ldp-reader-branch-trunk-toggle", trunkToggle.dataset.readerBranchToggle = String(view.postNumber), trunkToggle.tabIndex = -1, trunkToggle.setAttribute("aria-hidden", "true"), view.slots.body.append(trunkToggle));
	    const owned = Object.freeze({
	      view,
	      toggle,
	      count,
	      trunkToggle,
	      railToggles: /* @__PURE__ */ new Map()
	    });
	    return this.#owned.set(view.postNumber, owned), owned;
	  }
	  #syncSegmentedRailToggles(owned, children) {
	    const retained = /* @__PURE__ */ new Set();
	    for (const child of children) {
	      if (child.slots.root.classList.contains("ldp-virtual-ancestor-shell"))
	        continue;
	      retained.add(child.postNumber);
	      let railToggle = owned.railToggles.get(child.postNumber);
	      railToggle?.parentElement !== child.slots.root && (railToggle?.remove(), railToggle = child.slots.root.ownerDocument.createElement("button"), railToggle.type = "button", railToggle.className = "ldp-reader-branch-rail-toggle", railToggle.dataset.readerBranchToggle = String(
	        owned.view.postNumber
	      ), railToggle.tabIndex = -1, railToggle.setAttribute("aria-hidden", "true"), child.slots.root.prepend(railToggle), owned.railToggles.set(child.postNumber, railToggle));
	    }
	    for (const [postNumber, railToggle] of owned.railToggles)
	      retained.has(postNumber) || (railToggle.remove(), owned.railToggles.delete(postNumber));
	  }
	  #clearRailToggles(owned) {
	    for (const railToggle of owned.railToggles.values())
	      railToggle.remove();
	    owned.railToggles.clear();
	  }
	  #syncCollapsed(view, toggle) {
	    const collapsed = this.#isCollapsed(view.postNumber);
	    if (this.#renderMode === "segmented-css") {
	      const owner = collapsed ? view.slots.root : view.postNumber === 1 ? view.slots.replyTree : view.slots.actions.hidden ? view.slots.root : view.slots.actions;
	      toggle.parentElement !== owner && (owner === view.slots.root ? view.slots.root.insertBefore(toggle, view.slots.header) : owner === view.slots.replyTree ? view.slots.replyTree.prepend(toggle) : view.slots.actions.prepend(toggle));
	    }
	    this.#setClass(
	      view.slots.root,
	      "ldp-branch-parent-collapsed",
	      collapsed
	    ), view.slots.replyList.hidden !== collapsed && (view.slots.replyList.hidden = collapsed), collapsed && this.#clearPaths(view.slots);
	    const owned = this.#owned.get(view.postNumber), descendantCount = Math.max(
	      this.#subtreePostCount(view.postNumber) - 1,
	      1
	    );
	    owned && (owned.count.textContent = `(${descendantCount})`, owned.count.hidden = !collapsed, owned.trunkToggle && owned.trunkToggle.disabled !== collapsed && (owned.trunkToggle.disabled = collapsed));
	    for (const railToggle of owned?.railToggles.values() ?? [])
	      railToggle.disabled !== collapsed && (railToggle.disabled = collapsed);
	    const iconName = collapsed ? "plus" : "minus";
	    toggle.querySelector(":scope > .ldp-icon")?.classList.contains(`ldp-icon-${iconName}`) || toggle.replaceChildren((0, import_reader_icon.createReaderIcon)(
	      toggle.ownerDocument,
	      iconName
	    )), this.#setAttribute(toggle, "aria-expanded", String(!collapsed)), this.#setAttribute(
	      toggle,
	      "aria-label",
	      collapsed ? `展开 ${descendantCount} 条回复` : `收起 ${descendantCount} 条回复`
	    );
	  }
	  #subtreePostCount(parentPostNumber) {
	    const cached = this.#domOwner.topology.subtreePostCountOf?.(
	      parentPostNumber
	    );
	    if (cached !== void 0) return Math.max(1, cached);
	    const visited = /* @__PURE__ */ new Set(), pending = [parentPostNumber];
	    for (; pending.length; ) {
	      const postNumber = pending.pop();
	      if (visited.has(postNumber)) continue;
	      visited.add(postNumber);
	      const children = this.#domOwner.topology.childrenOf?.(postNumber) ?? this.#domOwner.views().filter(
	        (view) => this.#domOwner.topology.parentOf(view.postNumber) === postNumber
	      ).map((view) => view.postNumber);
	      pending.push(...children);
	    }
	    return Math.max(1, visited.size);
	  }
	  #isCollapsed(postNumber) {
	    return this.#readCollapsed?.(postNumber) ?? this.#collapsed.has(postNumber);
	  }
	  #setAttribute(element, name, value) {
	    element.getAttribute(name) !== value && element.setAttribute(name, value);
	  }
	  #setStyle(element, name, value) {
	    element.style.getPropertyValue(name) !== value && element.style.setProperty(name, value);
	  }
	  #setClass(element, name, enabled) {
	    element.classList.contains(name) !== enabled && element.classList.toggle(name, enabled);
	  }
	  #styleGeometryKey(root) {
	    return `${root.classList.contains("ldp-nested-preview") ? "nested" : "root"}|${root.getAttribute("style") ?? ""}`;
	  }
	  #cornerRadius(style) {
	    const value = style?.getPropertyValue("--ldp-reply-line-radius"), parsed = Number.parseFloat(value ?? "");
	    return Number.isFinite(parsed) ? Math.max(0, parsed) : 15;
	  }
	  #clear(view) {
	    this.#setClass(view.slots.root, "ldp-has-child-branches", !1), this.#setClass(view.slots.root, "ldp-branch-parent-collapsed", !1), view.slots.replyList.hidden && (view.slots.replyList.hidden = !1), this.#collapsed.delete(view.postNumber);
	    const owned = this.#owned.get(view.postNumber);
	    owned && (owned.toggle.hidden || (owned.toggle.hidden = !0), owned.count.hidden = !0, this.#clearRailToggles(owned)), this.#clearPaths(view.slots);
	  }
	  #clearPaths(slots) {
	    const visible = visiblePath(slots), hit = hitPath(slots);
	    visible.hasAttribute("d") && visible.removeAttribute("d"), hit.hasAttribute("d") && hit.removeAttribute("d"), slots.branchOverlay.hasAttribute("hidden") || slots.branchOverlay.setAttribute("hidden", ""), slots.branchOverlay.style.height && slots.branchOverlay.style.removeProperty("height"), slots.branchOverlay.hasAttribute("viewBox") && slots.branchOverlay.removeAttribute("viewBox");
	  }
	  #sweep() {
	    for (const [postNumber, owned] of this.#owned)
	      this.#domOwner.view(postNumber) !== owned.view && (this.#releaseOwned(owned), this.#owned.delete(postNumber));
	  }
	  #hasChildren(postNumber) {
	    return this.#domOwner.views().some(
	      (view) => this.#domOwner.topology.parentOf(view.postNumber) === postNumber
	    );
	  }
	  #restore() {
	    for (const view of this.#domOwner.views())
	      this.#setClass(
	        view.slots.root,
	        "ldp-segmented-branch-last",
	        !1
	      );
	    for (const owned of this.#owned.values())
	      this.#releaseOwned(owned);
	    this.#owned.clear(), this.#collapsed.clear();
	  }
	  #releaseOwned(owned) {
	    const slots = owned.view.slots;
	    slots.replyList.hidden = !1, this.#setClass(slots.root, "ldp-has-child-branches", !1), this.#setClass(slots.root, "ldp-branch-parent-collapsed", !1), this.#setClass(slots.root, "ldp-segmented-branch-last", !1), this.#clearPaths(slots), owned.toggle.remove(), owned.count.remove(), owned.trunkToggle?.remove(), this.#clearRailToggles(owned), slots.replyControls.classList.remove("ldp-branch-controls");
	  }
	}
}, "66dfd742354962c70761489b6ef31621512979cf03a30c3dca4b4260c023b364");

/* Source: lite/src/layout/reader-layout-style-controller.ts */
runtime.register("src/layout/reader-layout-style-controller.js", function(module, exports, require) {
	var reader_layout_style_controller_exports = {};
	__export(reader_layout_style_controller_exports, {
	  ReaderLayoutStyleController: () => ReaderLayoutStyleController,
	  readerPreferencesLayoutAdapter: () => readerPreferencesLayoutAdapter
	});
	module.exports = __toCommonJS(reader_layout_style_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_preferences_schema = require("../state/reader-preferences-schema.js");
	const readerPreferencesLayoutAdapter = Object.freeze({
	  readProfile: (preferences, mode) => mode === "fullpage" ? preferences.fullpageLayoutProfile : preferences.layoutProfile,
	  createPatch: (profile, mode) => mode === "fullpage" ? { fullpageLayoutProfile: profile } : { layoutProfile: profile }
	});
	function sameProfile(left, right) {
	  return import_reader_preferences_schema.READER_LAYOUT_REGIONS.every(
	    (region) => left[region] === right[region]
	  );
	}
	class ReaderLayoutStyleController {
	  scope;
	  changes = new import_signal.Signal();
	  #root;
	  #adapter;
	  #modePort;
	  #original = /* @__PURE__ */ new Map();
	  #previews = /* @__PURE__ */ new Map();
	  #preferences;
	  #mode;
	  #snapshot;
	  constructor(options) {
	    this.#root = options.root, this.#adapter = options.preferences, this.#modePort = options.mode, this.#preferences = options.readPreferences(), this.#mode = this.#modePort.read(), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    for (const region of import_reader_preferences_schema.READER_LAYOUT_REGIONS) {
	      const property = `--ldp-layout-${region}`;
	      this.#original.set(property, Object.freeze({
	        value: this.#root.style.getPropertyValue(property),
	        priority: typeof this.#root.style.getPropertyPriority == "function" ? this.#root.style.getPropertyPriority(property) : ""
	      }));
	    }
	    this.#snapshot = this.#commit(), options.preferenceChanges.subscribe((preferences) => {
	      this.#preferences = preferences, this.#publish();
	    }, this.scope), this.#modePort.subscribe((mode) => {
	      mode !== this.#mode && (this.#mode = mode, this.#publish());
	    }, this.scope), this.scope.add(() => {
	      this.changes.clear(), this.#previews.clear();
	      for (const [property, previous] of this.#original)
	        previous.value ? this.#root.style.setProperty(
	          property,
	          previous.value,
	          previous.priority
	        ) : this.#root.style.removeProperty(property);
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  get mode() {
	    return this.#mode;
	  }
	  profile(mode) {
	    return this.#adapter.readProfile(this.#preferences, mode);
	  }
	  readProfile(preferences, mode) {
	    return this.#adapter.readProfile(preferences, mode);
	  }
	  createPatch(profile, mode) {
	    return this.#adapter.createPatch(profile, mode);
	  }
	  preview(profile, mode = this.#mode) {
	    if (this.scope.destroyed) return;
	    const previous = this.#previews.get(mode);
	    previous && sameProfile(previous, profile) || (this.#previews.set(mode, Object.freeze({ ...profile })), mode === this.#mode && this.#publish());
	  }
	  clearPreview(mode) {
	    if (this.scope.destroyed) return;
	    if (mode) {
	      if (!this.#previews.delete(mode)) return;
	      mode === this.#mode && this.#publish();
	      return;
	    }
	    if (this.#previews.size === 0) return;
	    const currentChanged = this.#previews.has(this.#mode);
	    this.#previews.clear(), currentChanged && this.#publish();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #publish() {
	    this.#snapshot = this.#commit(), this.changes.emit(this.#snapshot);
	  }
	  #commit() {
	    const preview = this.#previews.get(this.#mode), profile = preview ?? this.profile(this.#mode);
	    for (const region of import_reader_preferences_schema.READER_LAYOUT_REGIONS)
	      this.#root.style.setProperty(
	        `--ldp-layout-${region}`,
	        `${profile[region]}%`
	      );
	    return Object.freeze({
	      mode: this.#mode,
	      profile,
	      previewing: !!preview
	    });
	  }
	}
}, "4feaeadcb3666626a6f01d91293edf33547d4b33c8ece5408edcd3840f98e1ee");

/* Source: lite/src/live/reader-topic-live-navigation-controller.ts */
runtime.register("src/live/reader-topic-live-navigation-controller.js", function(module, exports, require) {
	var reader_topic_live_navigation_controller_exports = {};
	__export(reader_topic_live_navigation_controller_exports, {
	  ReaderTopicLiveNavigationController: () => ReaderTopicLiveNavigationController
	});
	module.exports = __toCommonJS(reader_topic_live_navigation_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function postNumberFromChange(change) {
	  return change.kind !== "post" || change.created !== !0 || change.wasKnown === !0 ? null : (0, import_identifiers.tryDiscoursePostNumber)(
	    change.post.post_number
	  );
	}
	class ReaderTopicLiveNavigationController {
	  scope;
	  changes = new import_signal.Signal();
	  #navigation;
	  #onError;
	  #snapshot = Object.freeze({
	    nearEnd: !1,
	    pendingPostNumbers: Object.freeze([]),
	    targetPostNumber: null,
	    pendingCount: 0,
	    dismissed: !1,
	    jumping: !1
	  });
	  #jumpEpoch = 0;
	  constructor(options) {
	    this.#navigation = options.navigation, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), options.live.changes.subscribe((change) => {
	      const postNumber = postNumberFromChange(change);
	      if (postNumber !== null) {
	        if (this.#snapshot.nearEnd) {
	          this.#jump(postNumber, !0);
	          return;
	        }
	        this.#queue(postNumber);
	      }
	    }, this.scope), this.scope.add(() => {
	      this.#jumpEpoch += 1, this.changes.clear();
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  syncViewport(input) {
	    const nearEnd = input.atEnd === !0;
	    return this.#snapshot.nearEnd === nearEnd ? this.#snapshot : this.#commit({
	      ...this.#snapshot,
	      nearEnd
	    });
	  }
	  jumpPending() {
	    this.#assertActive();
	    const target = this.#snapshot.targetPostNumber;
	    return target === null ? Promise.resolve(null) : this.#jump(target, !1);
	  }
	  dismiss() {
	    return this.#assertActive(), this.#snapshot.pendingCount ? this.#commit({
	      ...this.#snapshot,
	      dismissed: !0
	    }) : this.#snapshot;
	  }
	  clear() {
	    return this.#assertActive(), this.#jumpEpoch += 1, this.#commit({
	      ...this.#snapshot,
	      pendingPostNumbers: Object.freeze([]),
	      targetPostNumber: null,
	      pendingCount: 0,
	      dismissed: !1,
	      jumping: !1
	    });
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #queue(postNumber) {
	    const pending = new Set(this.#snapshot.pendingPostNumbers);
	    pending.add(postNumber);
	    const pendingPostNumbers = Object.freeze(
	      [...pending].sort((left, right) => left - right)
	    );
	    this.#commit({
	      ...this.#snapshot,
	      pendingPostNumbers,
	      targetPostNumber: pendingPostNumbers[0] ?? null,
	      pendingCount: pendingPostNumbers.length,
	      dismissed: !1
	    });
	  }
	  async #jump(postNumber, automatic) {
	    this.#assertActive();
	    const epoch = ++this.#jumpEpoch, jumpedBatch = /* @__PURE__ */ new Set([
	      ...this.#snapshot.pendingPostNumbers,
	      postNumber
	    ]);
	    this.#commit({
	      ...this.#snapshot,
	      jumping: !0
	    });
	    try {
	      const result = await this.#navigation.navigate({
	        postNumber,
	        source: "message",
	        alignment: automatic ? "nearest" : "center",
	        highlight: !0
	      });
	      if (epoch !== this.#jumpEpoch || this.scope.destroyed) return result;
	      if (result.status === "revealed") {
	        const pendingPostNumbers = Object.freeze(
	          this.#snapshot.pendingPostNumbers.filter(
	            (pendingPostNumber) => !jumpedBatch.has(pendingPostNumber)
	          )
	        );
	        this.#commit({
	          ...this.#snapshot,
	          pendingPostNumbers,
	          targetPostNumber: pendingPostNumbers[0] ?? null,
	          pendingCount: pendingPostNumbers.length,
	          dismissed: pendingPostNumbers.length ? this.#snapshot.dismissed : !1,
	          jumping: !1
	        });
	      } else
	        this.#queue(postNumber), this.#commit({
	          ...this.#snapshot,
	          jumping: !1
	        });
	      return result;
	    } catch (cause) {
	      throw epoch === this.#jumpEpoch && !this.scope.destroyed && (this.#queue(postNumber), this.#commit({
	        ...this.#snapshot,
	        jumping: !1
	      }), this.#onError(cause)), cause;
	    }
	  }
	  #commit(input) {
	    const snapshot = Object.freeze({
	      nearEnd: input.nearEnd,
	      pendingPostNumbers: Object.freeze([...input.pendingPostNumbers]),
	      targetPostNumber: input.targetPostNumber,
	      pendingCount: input.pendingCount,
	      dismissed: input.dismissed,
	      jumping: input.jumping
	    });
	    this.#snapshot = snapshot;
	    for (const cause of this.changes.emit(snapshot)) this.#onError(cause);
	    return snapshot;
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderTopicLiveNavigationController 已销毁");
	  }
	}
}, "7f09e77612805e96f6be1113157d49f8b6fa5b96495168f9146e0245932a5067");

/* Source: lite/src/live/reader-topic-live-navigation-view.ts */
runtime.register("src/live/reader-topic-live-navigation-view.js", function(module, exports, require) {
	var reader_topic_live_navigation_view_exports = {};
	__export(reader_topic_live_navigation_view_exports, {
	  ReaderTopicLiveNavigationView: () => ReaderTopicLiveNavigationView
	});
	module.exports = __toCommonJS(reader_topic_live_navigation_view_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderTopicLiveNavigationView {
	  scope;
	  #navigation;
	  #elements;
	  #notify;
	  constructor(options) {
	    this.#navigation = options.navigation, this.#elements = options.elements, this.#notify = options.notify ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(this.#elements.jump, "click", () => {
	      this.#jump();
	    }), this.scope.listen(this.#elements.dismiss, "click", () => {
	      this.#navigation.dismiss();
	    }), this.#navigation.changes.subscribe((snapshot) => {
	      this.#sync(snapshot);
	    }, this.scope), this.scope.add(() => {
	      this.#elements.root.hidden = !0, this.#elements.root.removeAttribute("aria-busy"), this.#elements.label.textContent = "";
	    }), this.#sync(this.#navigation.snapshot);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #sync(snapshot) {
	    const hidden = snapshot.pendingCount <= 0 || snapshot.dismissed || snapshot.targetPostNumber === null;
	    this.#elements.root.hidden !== hidden && (this.#elements.root.hidden = hidden);
	    const busy = String(snapshot.jumping);
	    this.#elements.root.getAttribute("aria-busy") !== busy && this.#elements.root.setAttribute("aria-busy", busy);
	    const disabled = snapshot.jumping || hidden;
	    this.#elements.jump.disabled !== disabled && (this.#elements.jump.disabled = disabled), this.#elements.dismiss.disabled !== disabled && (this.#elements.dismiss.disabled = disabled);
	    const label = snapshot.pendingCount === 1 ? "查看 1 个新回复" : `查看 ${snapshot.pendingCount} 个新回复`, labelText = hidden ? "" : label;
	    this.#elements.label.textContent !== labelText && (this.#elements.label.textContent = labelText);
	    const ariaLabel = snapshot.targetPostNumber === null ? "查看新回复" : `${label},从楼层 #${snapshot.targetPostNumber} 开始`;
	    this.#elements.jump.getAttribute("aria-label") !== ariaLabel && this.#elements.jump.setAttribute("aria-label", ariaLabel);
	  }
	  #jump() {
	    this.#navigation.snapshot.jumping || this.#navigation.jumpPending().then((result) => {
	      this.scope.destroyed || result && result.status !== "revealed" && result.status !== "superseded" && this.#notify("新回复暂时无法定位,请重试");
	    }).catch(() => {
	      this.scope.destroyed || this.#notify("新回复加载失败,请重试");
	    });
	  }
	}
}, "169a4f1c1c301e73f26e1abb3ebb6237cfe32f43074316d856f0f023a5cb4516");

/* Source: lite/src/live/topic-live-controller.ts */
runtime.register("src/live/topic-live-controller.js", function(module, exports, require) {
	var topic_live_controller_exports = {};
	__export(topic_live_controller_exports, {
	  TopicLiveController: () => TopicLiveController,
	  normalizeTopicLiveMessage: () => normalizeTopicLiveMessage
	});
	module.exports = __toCommonJS(topic_live_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	const POST_REFRESH_MESSAGE_TYPES = /* @__PURE__ */ new Set([
	  "created",
	  "rebaked",
	  "recovered",
	  "revised"
	]), DELETION_MESSAGE_TYPES = /* @__PURE__ */ new Set(["deleted", "destroyed"]), TOPIC_STATS_MESSAGE_TYPE = "stats", BOOST_ADDED_MESSAGE_TYPE = "boost_added", BOOST_REMOVED_MESSAGE_TYPE = "boost_removed";
	function delay(value, fallback, name) {
	  const numeric = Number(value ?? fallback);
	  if (!Number.isFinite(numeric) || numeric < 0)
	    throw new RangeError(`${name} 必须是非负有限毫秒`);
	  return numeric;
	}
	function payloadFromMessage(message) {
	  if (!message || typeof message != "object") return {};
	  const record = message;
	  return record.payload && typeof record.payload == "object" ? record.payload : record;
	}
	function objectRecord(value) {
	  return value && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function positiveInteger(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function requestPressureFailure(error) {
	  const record = objectRecord(error), status = Number(record?.status ?? 0), code = String(record?.code ?? "").trim(), name = String(record?.name ?? "").trim();
	  return status === 429 || record?.cloudflareMitigated === !0 || name === "AbortError" || ["cancelled", "queue-limit"].includes(code);
	}
	function username(value) {
	  return String(value ?? "").trim().replace(/^@+/, "").toLocaleLowerCase();
	}
	function boostUser(boost) {
	  const user = objectRecord(boost.user);
	  return Object.freeze({
	    id: positiveInteger(user?.id ?? boost.user_id),
	    username: username(user?.username ?? boost.username)
	  });
	}
	function normalizeTopicLiveMessage(message, rawTopicId) {
	  const topicId = (0, import_identifiers.discourseTopicId)(rawTopicId), envelope = message && typeof message == "object" ? message : {}, payload = payloadFromMessage(message), messageTopicId = (0, import_identifiers.tryDiscourseTopicId)(
	    envelope.topic_id ?? payload.topic_id
	  );
	  if (messageTopicId !== null && messageTopicId !== topicId)
	    return Object.freeze({ kind: "ignore" });
	  const messageType = String(payload.type ?? "").trim(), reactionPostId = Array.isArray(payload.reactions) || messageType === "acted" ? (0, import_identifiers.tryDiscoursePostId)(payload.post_id ?? payload.id) : null, typedPostId = POST_REFRESH_MESSAGE_TYPES.has(messageType) || DELETION_MESSAGE_TYPES.has(messageType) ? (0, import_identifiers.tryDiscoursePostId)(payload.post_id) : null, postId = reactionPostId ?? typedPostId ?? (0, import_identifiers.tryDiscoursePostId)(payload.id);
	  if (messageType === "read") return Object.freeze({ kind: "ignore" });
	  if (reactionPostId !== null)
	    return Object.freeze({ kind: "reaction", postId: reactionPostId });
	  if (postId !== null && messageType === BOOST_ADDED_MESSAGE_TYPE) {
	    const boost = objectRecord(payload.boost);
	    if (boost && positiveInteger(boost.id) !== null)
	      return Object.freeze({ kind: "boost-added", postId, boost });
	  }
	  if (postId !== null && messageType === BOOST_REMOVED_MESSAGE_TYPE) {
	    const boostId = positiveInteger(payload.boost_id);
	    if (boostId !== null)
	      return Object.freeze({ kind: "boost-removed", postId, boostId });
	  }
	  return postId !== null && DELETION_MESSAGE_TYPES.has(messageType) ? Object.freeze({
	    kind: "post-delete",
	    postId
	  }) : postId !== null && POST_REFRESH_MESSAGE_TYPES.has(messageType) ? Object.freeze({
	    kind: "post",
	    postId,
	    created: messageType === "created"
	  }) : Object.freeze(messageType === TOPIC_STATS_MESSAGE_TYPE ? {
	    kind: "topic-stats",
	    postsCount: positiveInteger(payload.posts_count)
	  } : { kind: "ignore" });
	}
	class TopicLiveController {
	  topicId;
	  scope;
	  changes = new import_signal.Signal();
	  #messageBus;
	  #session;
	  #cache;
	  #currentUsername;
	  #postDelayMs;
	  #reactionDelayMs;
	  #reactionBatchSize;
	  #topicDelayMs;
	  #setTimer;
	  #clearTimer;
	  #onError;
	  #subscriptions = [];
	  #pendingPosts = /* @__PURE__ */ new Map();
	  #pendingReactionPostIds = /* @__PURE__ */ new Set();
	  #fullRefreshReasons = /* @__PURE__ */ new Set();
	  #tasks = /* @__PURE__ */ new Set();
	  #reactionRefreshTimer = 0;
	  #reactionRefreshRunning = !1;
	  #reactionRefreshSuppressed = !1;
	  #fullRefreshTimer = 0;
	  #fullRefreshRunning = !1;
	  #activationEpoch = 0;
	  #active = !1;
	  #closed = !1;
	  constructor(options) {
	    if (this.topicId = (0, import_identifiers.discourseTopicId)(options.topicId), options.session.topicId !== this.topicId)
	      throw new Error("TopicLiveController 与 TopicSession topicId 不一致");
	    this.#messageBus = options.messageBus, this.#session = options.session, this.#cache = options.cache ?? null, this.#currentUsername = username(options.currentUsername), this.#postDelayMs = delay(options.postDelayMs, 120, "postDelayMs"), this.#reactionDelayMs = delay(
	      options.reactionDelayMs,
	      1500,
	      "reactionDelayMs"
	    );
	    const reactionBatchSize = positiveInteger(options.reactionBatchSize ?? 20);
	    if (reactionBatchSize === null)
	      throw new RangeError("reactionBatchSize 必须是正安全整数");
	    this.#reactionBatchSize = reactionBatchSize, this.#topicDelayMs = delay(options.topicDelayMs, 350, "topicDelayMs"), this.#setTimer = options.setTimer ?? ((callback, milliseconds) => setTimeout(callback, milliseconds)), this.#clearTimer = options.clearTimer ?? clearTimeout, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.scope), this.scope.add(() => {
	      this.#closed = !0, this.#deactivate(), this.changes.clear();
	    });
	  }
	  get active() {
	    return this.#active;
	  }
	  start() {
	    if (this.#assertOpen(), this.#active) return !0;
	    const handler = (message) => {
	      this.#handleMessage(message);
	    }, topicSubscription = {
	      channel: `/topic/${this.topicId}`,
	      handler
	    };
	    try {
	      this.#messageBus.subscribe(
	        topicSubscription.channel,
	        topicSubscription.handler
	      );
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	    this.#subscriptions.push(topicSubscription);
	    const reactionSubscription = {
	      channel: `/topic/${this.topicId}/reactions`,
	      handler
	    };
	    try {
	      this.#messageBus.subscribe(
	        reactionSubscription.channel,
	        reactionSubscription.handler
	      ), this.#subscriptions.push(reactionSubscription);
	    } catch (error) {
	      this.#onError(error);
	    }
	    return this.#activationEpoch += 1, this.#active = !0, !0;
	  }
	  setActive(active, options = {}) {
	    if (this.#assertOpen(), !active)
	      return this.#deactivate(), !1;
	    const started = this.start();
	    return started && options.refresh === !0 && this.scheduleTopicRefresh("resume"), started;
	  }
	  scheduleTopicRefresh(reason = "explicit") {
	    if (!this.#active || this.#closed || (this.#fullRefreshReasons.add(String(reason || "explicit")), this.#fullRefreshTimer || this.#fullRefreshRunning)) return;
	    const epoch = this.#activationEpoch;
	    this.#fullRefreshTimer = this.#setTimer(() => {
	      this.#fullRefreshTimer = 0, this.#track(this.#refreshTopic(epoch));
	    }, this.#topicDelayMs);
	  }
	  /**
	   * 宿主 app-event 已携带完整 Post model 时直接提交当前 canonical。
	   * 这里只消费已加载楼层,不创建 stream、不发请求;缺少权威字段的 MessageBus
	   * 事件继续沿既有单帖刷新路径处理。
	   */
	  ingestPostDelta(value) {
	    if (!this.#active || this.#closed) return !1;
	    const ingest = this.#session.ingestPosts, delta = objectRecord(value), postId = (0, import_identifiers.tryDiscoursePostId)(delta?.id);
	    if (!ingest || !delta || postId === null) return !1;
	    const current = this.#session.postById(postId);
	    if (!current) return !1;
	    const currentRecord = current;
	    if ((0, import_identifiers.tryDiscourseTopicId)(
	      delta.topic_id ?? currentRecord.topic_id
	    ) !== this.topicId) return !1;
	    const next = Object.freeze({
	      ...currentRecord,
	      ...delta
	    });
	    try {
	      ingest.call(this.#session, [next], "message-bus");
	      const canonical = this.#session.postById(postId) ?? next;
	      return this.#emit(Object.freeze({
	        kind: "post",
	        postId,
	        post: canonical,
	        created: !1,
	        wasKnown: !0
	      })), this.#track(this.#invalidate([`post:${postId}`])), !0;
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  async flush() {
	    for (; this.#tasks.size; )
	      await Promise.allSettled([...this.#tasks]);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #handleMessage(message) {
	    if (!this.#active || this.#closed) return;
	    let normalized;
	    try {
	      normalized = normalizeTopicLiveMessage(message, this.topicId);
	    } catch (error) {
	      this.#onError(error);
	      return;
	    }
	    if (normalized.kind === "ignore") return;
	    if (normalized.kind === "reaction") {
	      this.#queueReactionRefresh(normalized.postId);
	      return;
	    }
	    if (normalized.kind === "topic-stats") {
	      this.#pendingPosts.size === 0 && this.#statsRequireTopicRefresh(normalized.postsCount) && this.scheduleTopicRefresh("stats");
	      return;
	    }
	    if (normalized.kind === "boost-added" || normalized.kind === "boost-removed") {
	      this.#commitBoostDelta(normalized);
	      return;
	    }
	    const postChange = normalized, known = this.#session.postById(postChange.postId) !== void 0;
	    if (postChange.kind === "post-delete" && !known || postChange.kind === "post" && !postChange.created && !known) return;
	    postChange.kind === "post" && postChange.created && this.#cancelStatsRefresh();
	    const current = this.#pendingPosts.get(postChange.postId);
	    if (current) {
	      if (postChange.kind === "post-delete")
	        current.operation = "delete", current.created = !1;
	      else {
	        const recoveringFromDelete = current.operation === "delete";
	        current.operation = "refresh", current.created = recoveringFromDelete ? postChange.created : current.created || postChange.created;
	      }
	      return;
	    }
	    const epoch = this.#activationEpoch, pending = {
	      postId: postChange.postId,
	      operation: postChange.kind === "post-delete" ? "delete" : "refresh",
	      created: postChange.kind === "post" && postChange.created,
	      timerId: this.#setTimer(() => {
	        this.#pendingPosts.delete(postChange.postId), this.#track(
	          pending.operation === "delete" ? this.#deletePost(postChange.postId, epoch) : this.#refreshPost(postChange.postId, pending.created, epoch)
	        );
	      }, this.#postDelayMs)
	    };
	    this.#pendingPosts.set(postChange.postId, pending);
	  }
	  #commitBoostDelta(message) {
	    const ingest = this.#session.ingestPosts, current = this.#session.postById(message.postId);
	    if (!ingest || !current) return !1;
	    const currentRecord = current, currentBoosts = Array.isArray(currentRecord.boosts) ? currentRecord.boosts.map((value) => objectRecord(value)).filter((value) => value !== null) : [];
	    let nextBoosts, ownBoostChanged = !1;
	    if (message.kind === "boost-added") {
	      const incomingIdentity = boostUser(message.boost);
	      let replaced = !1;
	      nextBoosts = Object.freeze(currentBoosts.map((boost) => {
	        const identity = boostUser(boost), sameBoost = positiveInteger(boost.id) === positiveInteger(message.boost.id), sameUser = incomingIdentity.id !== null && identity.id === incomingIdentity.id;
	        return !sameBoost && !sameUser ? boost : (replaced = !0, Object.freeze({ ...boost, ...message.boost }));
	      })), replaced || (nextBoosts = Object.freeze([
	        ...nextBoosts,
	        Object.freeze({ ...message.boost })
	      ])), ownBoostChanged = !!(this.#currentUsername && incomingIdentity.username === this.#currentUsername);
	    } else {
	      const removed = currentBoosts.find((boost) => positiveInteger(boost.id) === message.boostId);
	      nextBoosts = Object.freeze(currentBoosts.filter((boost) => positiveInteger(boost.id) !== message.boostId)), ownBoostChanged = !!(removed && this.#currentUsername && boostUser(removed).username === this.#currentUsername);
	    }
	    const next = Object.freeze({
	      ...currentRecord,
	      boosts: nextBoosts,
	      ...ownBoostChanged ? { can_boost: message.kind === "boost-removed" } : {}
	    });
	    try {
	      ingest.call(this.#session, [next], "message-bus");
	      const canonical = this.#session.postById(message.postId) ?? next;
	      return this.#emit(Object.freeze({
	        kind: "post",
	        postId: message.postId,
	        post: canonical,
	        created: !1,
	        wasKnown: !0
	      })), this.#track(this.#invalidate([`post:${message.postId}`])), !0;
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  async #deletePost(postId, epoch) {
	    if (this.#acceptsWork(epoch) && this.#session.postById(postId))
	      try {
	        const preserved = this.#session.preserveDeletedPostById(postId);
	        if (!this.#acceptsWork(epoch)) return;
	        preserved.topicArchived && this.#deactivate();
	      } catch (error) {
	        this.#onError(error);
	      }
	  }
	  async #refreshPost(postId, created, epoch) {
	    if (!this.#acceptsWork(epoch)) return;
	    const wasKnown = this.#session.postById(postId) !== void 0, createdPostMissing = created && !wasKnown;
	    if (!(!createdPostMissing && !wasKnown)) {
	      await this.#invalidate([`post:${postId}`]);
	      try {
	        const post = await this.#session.loadPostById(postId, {
	          background: !0,
	          created: createdPostMissing
	        });
	        if (!post || !this.#acceptsWork(epoch)) {
	          !post && createdPostMissing && this.#acceptsWork(epoch) && this.scheduleTopicRefresh("created-post-missing");
	          return;
	        }
	        this.#emit(Object.freeze({
	          kind: "post",
	          postId: (0, import_identifiers.discoursePostId)(postId),
	          post,
	          created: createdPostMissing,
	          wasKnown
	        }));
	      } catch (error) {
	        this.#onError(error), createdPostMissing && this.#acceptsWork(epoch) && !requestPressureFailure(error) && this.scheduleTopicRefresh("created-post-refresh-failed");
	      }
	    }
	  }
	  #queueReactionRefresh(postId) {
	    if (this.#reactionRefreshSuppressed || !this.#session.loadPostsByIds || !this.#session.postById(postId) || (this.#pendingReactionPostIds.delete(postId), this.#pendingReactionPostIds.add(postId), this.#reactionRefreshRunning)) return;
	    this.#reactionRefreshTimer && this.#clearTimer(this.#reactionRefreshTimer);
	    const epoch = this.#activationEpoch;
	    this.#reactionRefreshTimer = this.#setTimer(() => {
	      this.#reactionRefreshTimer = 0, this.#track(this.#refreshReactions(epoch));
	    }, this.#reactionDelayMs);
	  }
	  async #refreshReactions(epoch) {
	    const loadPostsByIds = this.#session.loadPostsByIds;
	    if (!this.#acceptsWork(epoch) || this.#reactionRefreshRunning || this.#reactionRefreshSuppressed || !loadPostsByIds) return;
	    const pending = [...this.#pendingReactionPostIds];
	    this.#pendingReactionPostIds.clear();
	    const postIds = pending.slice(-this.#reactionBatchSize).filter((postId) => this.#session.postById(postId) !== void 0);
	    if (postIds.length) {
	      this.#reactionRefreshRunning = !0;
	      try {
	        await this.#invalidate(postIds.map((postId) => `post:${postId}`));
	        const result = await loadPostsByIds.call(this.#session, postIds, {
	          background: !0,
	          refresh: !0,
	          maxAttempts: 1,
	          ingestSource: "target-refresh"
	        });
	        if (!this.#acceptsWork(epoch)) return;
	        const requested = new Set(postIds);
	        for (const post of result.posts) {
	          const postId = (0, import_identifiers.tryDiscoursePostId)(post.id);
	          postId === null || !requested.has(postId) || this.#emit(Object.freeze({
	            kind: "post",
	            postId,
	            post,
	            created: !1,
	            wasKnown: !0
	          }));
	        }
	      } catch (error) {
	        this.#acceptsWork(epoch) && (this.#reactionRefreshSuppressed = !0, this.#pendingReactionPostIds.clear(), this.#onError(error));
	      } finally {
	        if (this.#reactionRefreshRunning = !1, this.#active && !this.#closed && !this.#reactionRefreshSuppressed && this.#pendingReactionPostIds.size) {
	          const postId = [...this.#pendingReactionPostIds].at(-1);
	          postId !== void 0 && this.#queueReactionRefresh(postId);
	        }
	      }
	    }
	  }
	  #statsRequireTopicRefresh(postsCount) {
	    if (postsCount === null || !this.#session.streamPostIds) return !1;
	    try {
	      return postsCount > this.#session.streamPostIds().length;
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  async #refreshTopic(epoch) {
	    if (!this.#acceptsWork(epoch) || this.#fullRefreshRunning) return;
	    this.#fullRefreshRunning = !0;
	    const reasons = Object.freeze([...this.#fullRefreshReasons].sort());
	    this.#fullRefreshReasons.clear();
	    try {
	      const topic = await this.#session.refresh({ background: !0 });
	      this.#acceptsWork(epoch) && this.#emit(Object.freeze({ kind: "topic", topic, reasons }));
	    } catch (error) {
	      this.#onError(error);
	    } finally {
	      this.#fullRefreshRunning = !1, this.#active && !this.#closed && this.#fullRefreshReasons.size && !this.#fullRefreshTimer && this.scheduleTopicRefresh("coalesced");
	    }
	  }
	  async #invalidate(tags) {
	    if (this.#cache)
	      try {
	        await this.#cache.invalidate({ tags });
	      } catch (error) {
	        this.#onError(error);
	      }
	  }
	  #emit(change) {
	    for (const error of this.changes.emit(change)) this.#onError(error);
	  }
	  #track(task) {
	    this.#tasks.add(task), task.finally(() => {
	      this.#tasks.delete(task);
	    }).catch(() => {
	    });
	  }
	  #cancelStatsRefresh() {
	    this.#fullRefreshReasons.delete("stats") && (this.#fullRefreshReasons.size > 0 || !this.#fullRefreshTimer || (this.#clearTimer(this.#fullRefreshTimer), this.#fullRefreshTimer = 0));
	  }
	  #deactivate() {
	    this.#activationEpoch += 1, this.#active = !1, this.#fullRefreshTimer && this.#clearTimer(this.#fullRefreshTimer), this.#fullRefreshTimer = 0, this.#fullRefreshReasons.clear(), this.#reactionRefreshTimer && this.#clearTimer(this.#reactionRefreshTimer), this.#reactionRefreshTimer = 0, this.#pendingReactionPostIds.clear();
	    for (const pending of this.#pendingPosts.values())
	      this.#clearTimer(pending.timerId);
	    this.#pendingPosts.clear();
	    for (const subscription of this.#subscriptions.splice(0).reverse())
	      try {
	        this.#messageBus.unsubscribe(subscription.channel, subscription.handler);
	      } catch (error) {
	        this.#onError(error);
	      }
	  }
	  #assertOpen() {
	    if (this.#closed) throw new Error("TopicLiveController 已销毁");
	  }
	  #acceptsWork(epoch) {
	    return this.#active && !this.#closed && epoch === this.#activationEpoch;
	  }
	}
}, "0f86bfc921f880ef5e0c6c76f729d5161c57bf44a2700b4415cfbcd675b43d60");

/* Source: lite/src/shell/embedded-host-appearance.ts */
runtime.register("src/shell/embedded-host-appearance.js", function(module, exports, require) {
	var embedded_host_appearance_exports = {};
	__export(embedded_host_appearance_exports, {
	  EmbeddedHostAppearanceController: () => EmbeddedHostAppearanceController,
	  measureEmbeddedHostTopicRowHeight: () => measureEmbeddedHostTopicRowHeight
	});
	module.exports = __toCommonJS(embedded_host_appearance_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_reader_workspace = require("./reader-workspace.js");
	const STYLE_PROPERTIES = Object.freeze([
	  "--ldp-reader-host-min-width",
	  "--ldp-reader-native-row-height"
	]), ROOT_STYLE_PROPERTIES = Object.freeze([
	  "--ldp-reader-list-zebra-color",
	  "--ldp-divider-line-color",
	  "--ldp-divider-line-width"
	]);
	function measureEmbeddedHostTopicRowHeight(documentPort, overlay) {
	  const heights = [...documentPort.querySelectorAll(
	    "tr.topic-list-item,.topic-list-item,.latest-topic-list-item"
	  )].filter((node) => !node.closest(".ldp-overlay") && !overlay.contains(node)).map((node) => {
	    const rect = node.getBoundingClientRect();
	    return rect.width > 0 && rect.height >= 48 && rect.height <= 320 ? Math.round(rect.height) : 0;
	  }).filter((height) => height > 0).sort((left, right) => left - right);
	  return heights.length ? heights[Math.floor((heights.length - 1) / 2)] : 0;
	}
	class EmbeddedHostAppearanceController {
	  scope;
	  #workspace;
	  #pageRoot;
	  #overlay;
	  #readAppearance;
	  #measureRowHeight;
	  #hostMinWidth;
	  #active = !1;
	  #rowHeight = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#workspace = options.workspace, this.#pageRoot = options.pageRoot, this.#overlay = options.overlay, this.#readAppearance = options.readAppearance, this.#measureRowHeight = options.measureRowHeight ?? (() => measureEmbeddedHostTopicRowHeight(
	      this.#pageRoot.ownerDocument,
	      this.#overlay
	    )), this.#hostMinWidth = Math.max(
	      1,
	      Math.round(options.hostMinWidth ?? import_reader_workspace.READER_HOST_MIN_WIDTH)
	    ), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#workspace.changes.subscribe(() => this.#syncActivation(), this.scope), options.appearanceChanges?.subscribe(() => {
	      this.#active && this.#applyAppearance();
	    }, this.scope), this.scope.add(() => this.#clear()), this.#syncActivation();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #syncActivation() {
	    if (this.#destroyed) return;
	    const active = this.#workspace.snapshot.presentation.embedded;
	    active && !this.#active ? (this.#active = !0, this.#rowHeight = Math.max(0, Math.round(this.#measureRowHeight())), this.#applyAppearance()) : !active && this.#active && (this.#active = !1, this.#clear());
	  }
	  #applyAppearance() {
	    const appearance = this.#readAppearance(), profile = appearance.profile, dark = appearance.theme === "dark", zebraColor = dark ? profile.listZebraColorDark : profile.listZebraColor, dividerColor = dark ? profile.dividerLineColorDark : profile.dividerLineColor;
	    for (const target of [this.#pageRoot, this.#overlay])
	      target.style.setProperty(
	        "--ldp-reader-host-min-width",
	        `${this.#hostMinWidth}px`
	      ), this.#rowHeight ? target.style.setProperty(
	        "--ldp-reader-native-row-height",
	        `${this.#rowHeight}px`
	      ) : target.style.removeProperty("--ldp-reader-native-row-height");
	    this.#pageRoot.style.setProperty("--ldp-reader-list-zebra-color", zebraColor), this.#pageRoot.classList.toggle(
	      "ldp-structure-colors-disabled",
	      !profile.structureColorsEnabled
	    ), !profile.structureColorsEnabled || dividerColor === appearance.defaultDividerLineColor ? this.#pageRoot.style.removeProperty("--ldp-divider-line-color") : this.#pageRoot.style.setProperty("--ldp-divider-line-color", dividerColor), profile.dividerLineWidth === appearance.defaultDividerLineWidth ? this.#pageRoot.style.removeProperty("--ldp-divider-line-width") : this.#pageRoot.style.setProperty(
	      "--ldp-divider-line-width",
	      `${profile.dividerLineWidth}px`
	    );
	  }
	  #clear() {
	    for (const target of [this.#pageRoot, this.#overlay])
	      for (const property of STYLE_PROPERTIES) target.style.removeProperty(property);
	    for (const property of ROOT_STYLE_PROPERTIES)
	      this.#pageRoot.style.removeProperty(property);
	    this.#pageRoot.classList.remove("ldp-structure-colors-disabled"), this.#rowHeight = 0;
	  }
	}
}, "551dfb96890751093acf088ff55afc564c71121e94330d07eb3be899bbdb95f4");

/* Source: lite/src/shell/embedded-host-root-controller.ts */
runtime.register("src/shell/embedded-host-root-controller.js", function(module, exports, require) {
	var embedded_host_root_controller_exports = {};
	__export(embedded_host_root_controller_exports, {
	  EmbeddedHostRootController: () => EmbeddedHostRootController
	});
	module.exports = __toCommonJS(embedded_host_root_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	const CARD_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item", ROOT_QUERIES = Object.freeze({
	  shell: "#ember-app,#main-outlet-wrapper,#main-outlet,.list-container,.topic-list,.navigation-container",
	  header: ".d-header,body > header",
	  sidebar: "#d-sidebar,.sidebar-wrapper,.sidebar-container"
	}), ROOT_RANK = Object.freeze({
	  sidebar: 1,
	  header: 2,
	  shell: 3
	});
	function elementFromNode(node) {
	  return node ? node.nodeType === 1 ? node : node.parentElement : null;
	}
	function topLevelBodyChild(documentPort, node) {
	  let current = elementFromNode(node);
	  for (; current?.parentElement && current.parentElement !== documentPort.body; )
	    current = current.parentElement;
	  return current?.parentElement === documentPort.body ? current : null;
	}
	class EmbeddedHostRootController {
	  scope;
	  #model;
	  #routeKind;
	  #document;
	  #overlay;
	  #mutations;
	  #enhancements;
	  #requestFrame;
	  #cancelFrame;
	  #activeScope = null;
	  #roots = /* @__PURE__ */ new Map();
	  #changedCards = /* @__PURE__ */ new Set();
	  #activityCards = /* @__PURE__ */ new Set();
	  #rootFrame = 0;
	  #cardFrame = 0;
	  #projectionMode = "actions-only";
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#routeKind = options.routeKind, this.#document = options.document, this.#overlay = options.overlay, this.#mutations = options.mutations, this.#enhancements = options.enhancements, this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#model.changes.subscribe(() => this.#syncActivation(), this.scope), options.topicFilterChanges?.subscribe(() => {
	      this.#scheduleRootSync();
	    }, this.scope), this.scope.add(() => this.#deactivate()), this.#syncActivation();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #syncActivation() {
	    if (this.#destroyed) return;
	    const embedded = this.#model.snapshot.presentation.embedded, shouldActivate = this.#routeKind === "list", projectionMode = embedded ? "embedded" : "actions-only";
	    if (this.#activeScope && (!shouldActivate || projectionMode !== this.#projectionMode) && this.#deactivate(), shouldActivate && !this.#activeScope) {
	      this.#projectionMode = projectionMode;
	      const activeScope = this.scope.child();
	      this.#activeScope = activeScope, this.#mutations.subscribe((batch) => this.#onMutations(batch), activeScope), this.#scheduleRootSync();
	    } else this.#activeScope && this.#scheduleRootSync();
	  }
	  #deactivate() {
	    const activeScope = this.#activeScope;
	    this.#activeScope = null, activeScope?.destroy(), this.#rootFrame && this.#cancelFrame(this.#rootFrame), this.#cardFrame && this.#cancelFrame(this.#cardFrame), this.#rootFrame = 0, this.#cardFrame = 0, this.#changedCards.clear(), this.#activityCards.clear();
	    for (const root of this.#roots.keys())
	      root.removeAttribute("data-ldp-reader-host-root");
	    this.#roots.clear(), this.#enhancements.clear();
	  }
	  #onMutations(batch) {
	    if (!this.#activeScope) return;
	    const hostRootsMayHaveChanged = batch.records.some(
	      (record) => record.type === "childList" && (record.target === this.#document.body || !batch.root?.contains(record.target))
	    );
	    (batch.rootChanged || hostRootsMayHaveChanged) && this.#scheduleRootSync();
	    for (const record of batch.records) this.#collectRecord(record);
	    this.#scheduleCardSync();
	  }
	  #collectRecord(record) {
	    if (record.type === "childList") {
	      const target = elementFromNode(record.target), activityTarget = target?.matches(".relative-date") && target.closest("td.activity"), changedNodes = [...record.addedNodes, ...record.removedNodes];
	      if (activityTarget && record.addedNodes.length > 0 && record.removedNodes.length > 0 && changedNodes.every((node) => node.nodeType === 3)) {
	        const card = activityTarget.closest(CARD_SELECTOR);
	        card && this.#activityCards.add(card);
	        return;
	      }
	      changedNodes.length > 0 && changedNodes.every((node) => {
	        const element = elementFromNode(node);
	        return !!(element?.matches(".ldp-topic-stats-component") || element?.closest(".ldp-topic-stats-component"));
	      }) || this.#collectNearestCard(record.target);
	      for (const node of record.addedNodes) this.#collectAddedCards(node);
	    } else
	      this.#collectNearestCard(record.target);
	  }
	  #collectNearestCard(node) {
	    const element = elementFromNode(node);
	    if (!element || element.closest(".ldp-overlay,.ldp-topic-stats-component")) return;
	    const card = element.matches(CARD_SELECTOR) ? element : element.closest(CARD_SELECTOR);
	    card && this.#changedCards.add(card);
	  }
	  #collectAddedCards(node) {
	    const element = elementFromNode(node);
	    if (!element || element.closest(".ldp-overlay,.ldp-topic-stats-component")) return;
	    const nearest = element.matches(CARD_SELECTOR) ? element : element.closest(CARD_SELECTOR);
	    if (nearest) {
	      this.#changedCards.add(nearest);
	      return;
	    }
	    for (const card of element.querySelectorAll(CARD_SELECTOR))
	      this.#changedCards.add(card);
	  }
	  #scheduleRootSync() {
	    this.#rootFrame || !this.#activeScope || (this.#rootFrame = this.#requestFrame(() => this.#syncRoots()));
	  }
	  #syncRoots() {
	    if (this.#rootFrame = 0, !this.#activeScope || !this.#document.body) return;
	    const next = /* @__PURE__ */ new Map(), mark = (node, role) => {
	      const root = topLevelBodyChild(this.#document, node);
	      if (!root || root === this.#overlay || root.contains(this.#overlay)) return;
	      const previous = next.get(root);
	      (!previous || ROOT_RANK[role] > ROOT_RANK[previous]) && next.set(root, role);
	    };
	    for (const role of ["shell", "header", "sidebar"])
	      for (const node of this.#document.querySelectorAll(ROOT_QUERIES[role]))
	        mark(node, role);
	    for (const [root, previousRole] of this.#roots) {
	      const nextRole = next.get(root);
	      previousRole === "shell" && nextRole !== "shell" && this.#enhancements.releaseRoot(root), (!nextRole || this.#projectionMode !== "embedded") && root.removeAttribute("data-ldp-reader-host-root");
	    }
	    for (const [root, role] of next)
	      this.#projectionMode === "embedded" ? root.setAttribute("data-ldp-reader-host-root", role) : root.removeAttribute("data-ldp-reader-host-root"), role === "shell" && this.#enhancements.syncRoot(root, this.#projectionMode);
	    this.#roots = next;
	  }
	  #scheduleCardSync() {
	    this.#cardFrame || !this.#activeScope || !this.#changedCards.size && !this.#activityCards.size || (this.#cardFrame = this.#requestFrame(() => this.#flushCards()));
	  }
	  #flushCards() {
	    if (this.#cardFrame = 0, !this.#activeScope) return;
	    for (const card of this.#activityCards) {
	      if (this.#projectionMode !== "embedded") {
	        this.#changedCards.add(card);
	        continue;
	      }
	      card.isConnected && !this.#changedCards.has(card) && !this.#enhancements.syncActivity(card) && this.#changedCards.add(card);
	    }
	    this.#activityCards.clear();
	    const cards = [...this.#changedCards].filter((card) => card.isConnected);
	    this.#changedCards.clear(), cards.length && this.#enhancements.syncCards(
	      Object.freeze(cards),
	      this.#projectionMode
	    );
	  }
	}
}, "ae436bc0bdb5a5548cdb320f289d72a5869aab60509f2decc3f5053ef275c4b5");

/* Source: lite/src/shell/embedded-host-scrollbar.ts */
runtime.register("src/shell/embedded-host-scrollbar.js", function(module, exports, require) {
	var embedded_host_scrollbar_exports = {};
	__export(embedded_host_scrollbar_exports, {
	  EmbeddedHostScrollbarController: () => EmbeddedHostScrollbarController,
	  EmbeddedHostScrollbarModel: () => EmbeddedHostScrollbarModel
	});
	module.exports = __toCommonJS(embedded_host_scrollbar_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	const MIN_THUMB_HEIGHT = 24, KEY_LINE_STEP = 40, KEY_PAGE_RATIO = 0.9;
	function finite(value, fallback) {
	  return Number.isFinite(value) ? value : fallback;
	}
	function clamp(value, minimum, maximum) {
	  return Math.max(minimum, Math.min(maximum, value));
	}
	function roundedCssPixels(value) {
	  return `${Math.round(value * 100) / 100}px`;
	}
	function emptySnapshot() {
	  return Object.freeze({
	    viewportHeight: 1,
	    scrollHeight: 1,
	    scrollTop: 0,
	    maxScroll: 0,
	    trackHeight: 1,
	    thumbHeight: 1,
	    maxThumbTop: 0,
	    thumbTop: 0,
	    inactive: !0
	  });
	}
	class EmbeddedHostScrollbarModel {
	  changes = new import_signal.Signal();
	  #snapshot = emptySnapshot();
	  get snapshot() {
	    return this.#snapshot;
	  }
	  update(rawMetrics, rawTrackHeight) {
	    const viewportHeight = Math.max(1, finite(rawMetrics.viewportHeight, 1)), scrollHeight = Math.max(
	      viewportHeight,
	      finite(rawMetrics.scrollHeight, viewportHeight)
	    ), maxScroll = Math.max(0, scrollHeight - viewportHeight), scrollTop = clamp(finite(rawMetrics.scrollTop, 0), 0, maxScroll), trackHeight = Math.max(1, finite(rawTrackHeight, 1)), thumbHeight = maxScroll > 0 ? Math.min(
	      trackHeight,
	      Math.max(MIN_THUMB_HEIGHT, trackHeight * viewportHeight / scrollHeight)
	    ) : trackHeight, maxThumbTop = Math.max(0, trackHeight - thumbHeight), thumbTop = maxScroll > 0 ? maxThumbTop * scrollTop / maxScroll : 0, next = Object.freeze({
	      viewportHeight,
	      scrollHeight,
	      scrollTop,
	      maxScroll,
	      trackHeight,
	      thumbHeight,
	      maxThumbTop,
	      thumbTop,
	      inactive: maxScroll <= 0
	    }), previous = this.#snapshot;
	    return next.viewportHeight === previous.viewportHeight && next.scrollHeight === previous.scrollHeight && next.scrollTop === previous.scrollTop && next.trackHeight === previous.trackHeight ? previous : (this.#snapshot = next, this.changes.emit(next), next);
	  }
	  scrollTopForPointer(clientY, trackTop, pointerOffsetY) {
	    const snapshot = this.#snapshot;
	    if (snapshot.maxScroll <= 0 || snapshot.maxThumbTop <= 0) return 0;
	    const thumbTop = clamp(
	      clientY - trackTop - pointerOffsetY,
	      0,
	      snapshot.maxThumbTop
	    );
	    return snapshot.maxScroll * thumbTop / snapshot.maxThumbTop;
	  }
	  scrollTopForKey(key) {
	    const snapshot = this.#snapshot;
	    let next = snapshot.scrollTop;
	    if (key === "ArrowUp") next -= KEY_LINE_STEP;
	    else if (key === "ArrowDown") next += KEY_LINE_STEP;
	    else if (key === "PageUp") next -= snapshot.viewportHeight * KEY_PAGE_RATIO;
	    else if (key === "PageDown") next += snapshot.viewportHeight * KEY_PAGE_RATIO;
	    else if (key === "Home") next = 0;
	    else if (key === "End") next = snapshot.maxScroll;
	    else return null;
	    return clamp(next, 0, snapshot.maxScroll);
	  }
	  reset() {
	    const next = emptySnapshot();
	    return this.#snapshot = next, this.changes.emit(next), next;
	  }
	}
	function pointerEventTarget(event) {
	  return event.target;
	}
	class EmbeddedHostScrollbarController {
	  scope;
	  model = new EmbeddedHostScrollbarModel();
	  #workspace;
	  #track;
	  #thumb;
	  #scroll;
	  #readScrollTop;
	  #readTrack;
	  #requestFrame;
	  #cancelFrame;
	  #createResizeObserver;
	  #resizeTargets;
	  #resizeObserver = null;
	  #frame = 0;
	  #pointer = null;
	  #active = !1;
	  #geometryDirty = !0;
	  #trackGeometry = { top: 0, height: 1 };
	  #scrollMetrics = null;
	  #destroyed = !1;
	  constructor(options) {
	    this.#workspace = options.workspace, this.#track = options.track, this.#thumb = options.thumb, this.#scroll = options.scroll, this.#readScrollTop = options.scroll.readScrollTop ?? (() => options.scroll.read().scrollTop), this.#resizeTargets = Object.freeze([
	      ...options.resizeTargets ?? [],
	      options.track
	    ]), this.#createResizeObserver = options.createResizeObserver, this.#readTrack = options.readTrack ?? (() => ({ top: this.#track.getBoundingClientRect().top, height: Math.max(1, this.#track.clientHeight) })), this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#workspace.changes.subscribe(() => this.#syncActivation(), this.scope), this.scope.listen(options.scrollTarget, "scroll", () => this.#schedule(), {
	      passive: !0
	    }), this.scope.listen(this.#track, "pointerdown", (event) => {
	      this.#onPointerDown(event);
	    }), this.scope.listen(this.#track, "pointermove", (event) => {
	      this.#onPointerMove(event);
	    });
	    for (const type of ["pointerup", "pointercancel", "lostpointercapture"])
	      this.scope.listen(this.#track, type, (event) => {
	        this.#stopPointer(event);
	      });
	    this.scope.listen(this.#track, "keydown", (event) => {
	      this.#onKeyDown(event);
	    }), this.scope.add(() => this.#deactivate()), this.#syncActivation();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  invalidateGeometry() {
	    this.#geometryDirty = !0, this.#schedule();
	  }
	  #syncActivation() {
	    if (this.#destroyed) return;
	    const active = this.#workspace.snapshot.presentation.embedded;
	    active !== this.#active && (active ? this.#activate() : this.#deactivate());
	  }
	  #activate() {
	    if (this.#active = !0, this.#geometryDirty = !0, !this.#resizeObserver && this.#createResizeObserver) {
	      this.#resizeObserver = this.#createResizeObserver(() => this.invalidateGeometry());
	      for (const target of this.#resizeTargets) this.#resizeObserver.observe(target);
	    }
	    this.#schedule();
	  }
	  #deactivate() {
	    this.#active = !1, this.#frame && this.#cancelFrame(this.#frame), this.#frame = 0, this.#resizeObserver?.disconnect(), this.#resizeObserver = null, this.#stopPointer(), this.#geometryDirty = !0, this.#trackGeometry = { top: 0, height: 1 }, this.#scrollMetrics = null, this.model.reset(), this.#clearDom();
	  }
	  #schedule() {
	    !this.#active || this.#frame || (this.#frame = this.#requestFrame(() => this.#sync()));
	  }
	  #sync() {
	    if (this.#frame = 0, !this.#active) return;
	    const geometryDirty = this.#geometryDirty;
	    if (geometryDirty) {
	      const geometry = this.#readTrack();
	      this.#trackGeometry = {
	        top: finite(geometry.top, 0),
	        height: Math.max(1, finite(geometry.height, 1))
	      }, this.#geometryDirty = !1;
	    }
	    const metrics = geometryDirty || this.#scrollMetrics === null ? this.#scroll.read() : {
	      ...this.#scrollMetrics,
	      scrollTop: this.#readScrollTop()
	    };
	    this.#scrollMetrics = metrics;
	    const snapshot = this.model.update(metrics, this.#trackGeometry.height);
	    this.#apply(snapshot);
	  }
	  #apply(snapshot) {
	    this.#track.classList.toggle(
	      "ldp-reader-host-scrollbar-inactive",
	      snapshot.inactive
	    ), this.#track.setAttribute("aria-disabled", String(snapshot.inactive)), this.#track.setAttribute("aria-valuemin", "0"), this.#track.setAttribute("aria-valuemax", String(Math.round(snapshot.maxScroll))), this.#track.setAttribute("aria-valuenow", String(Math.round(snapshot.scrollTop))), this.#thumb.style.height = roundedCssPixels(snapshot.thumbHeight), this.#thumb.style.transform = `translateY(${roundedCssPixels(snapshot.thumbTop)})`;
	  }
	  #clearDom() {
	    this.#track.classList.remove("ldp-reader-host-scrollbar-dragging"), this.#track.classList.add("ldp-reader-host-scrollbar-inactive"), this.#track.setAttribute("aria-disabled", "true"), this.#track.setAttribute("aria-valuemin", "0"), this.#track.setAttribute("aria-valuemax", "0"), this.#track.setAttribute("aria-valuenow", "0"), this.#thumb.style.removeProperty("height"), this.#thumb.style.removeProperty("transform");
	  }
	  #onPointerDown(event) {
	    if (event.button !== 0 || !this.#active) return;
	    this.#frame && this.#cancelFrame(this.#frame), this.#frame = 0, this.#geometryDirty = !0, this.#sync();
	    const snapshot = this.model.snapshot;
	    if (snapshot.inactive) return;
	    event.preventDefault(), event.stopPropagation();
	    const onThumb = pointerEventTarget(event) === this.#thumb, thumbRect = this.#thumb.getBoundingClientRect();
	    this.#pointer = {
	      pointerId: event.pointerId,
	      offsetY: onThumb ? event.clientY - thumbRect.top : snapshot.thumbHeight / 2
	    }, this.#track.classList.add("ldp-reader-host-scrollbar-dragging");
	    try {
	      this.#track.setPointerCapture(event.pointerId);
	    } catch {
	    }
	    try {
	      this.#track.focus({ preventScroll: !0 });
	    } catch {
	      this.#track.focus();
	    }
	    this.#scrollFromPointer(event.clientY);
	  }
	  #onPointerMove(event) {
	    !this.#pointer || event.pointerId !== this.#pointer.pointerId || (event.preventDefault(), this.#scrollFromPointer(event.clientY));
	  }
	  #scrollFromPointer(clientY) {
	    this.#pointer && this.#scroll.scrollTo(this.model.scrollTopForPointer(
	      clientY,
	      this.#trackGeometry.top,
	      this.#pointer.offsetY
	    ));
	  }
	  #stopPointer(event) {
	    if (!this.#pointer || event && event.pointerId !== this.#pointer.pointerId)
	      return;
	    const pointerId = this.#pointer.pointerId;
	    this.#pointer = null, this.#track.classList.remove("ldp-reader-host-scrollbar-dragging");
	    try {
	      this.#track.hasPointerCapture(pointerId) && this.#track.releasePointerCapture(pointerId);
	    } catch {
	    }
	  }
	  #onKeyDown(event) {
	    if (!this.#active) return;
	    this.#sync();
	    const top = this.model.scrollTopForKey(event.key);
	    top !== null && (event.preventDefault(), event.stopPropagation(), this.#scroll.scrollTo(top));
	  }
	}
}, "7cf7ab170b59d2e47120ded04d6003ac077b5a456666267e473b368e48f45a9b");

/* Source: lite/src/shell/embedded-host-top-shortcut.ts */
runtime.register("src/shell/embedded-host-top-shortcut.js", function(module, exports, require) {
	var embedded_host_top_shortcut_exports = {};
	__export(embedded_host_top_shortcut_exports, {
	  EmbeddedHostTopShortcutController: () => EmbeddedHostTopShortcutController
	});
	module.exports = __toCommonJS(embedded_host_top_shortcut_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	const TOP_EDGE = 8, BUTTON_SIZE = 30, POINTER_GAP = 12, UPWARD_DISTANCE = 240, UPWARD_WINDOW_MS = 450, UPWARD_BREAK_MS = 240, SHOW_DURATION_MS = 3e3;
	class EmbeddedHostTopShortcutController {
	  scope;
	  #workspace;
	  #button;
	  #readScrollTop;
	  #readViewportHeight;
	  #scrollToTop;
	  #now;
	  #setTimeout;
	  #clearTimeout;
	  #setInterval;
	  #clearInterval;
	  #requestFrame;
	  #cancelFrame;
	  #hideTimer = 0;
	  #countdownTimer = 0;
	  #jumpFrame = 0;
	  #scrollTop = 0;
	  #upwardDistance = 0;
	  #upwardStartedAt = 0;
	  #lastScrollAt = 0;
	  #pointerX = Number.NaN;
	  #pointerY = Number.NaN;
	  #jumping = !1;
	  #active = !1;
	  #destroyed = !1;
	  constructor(options) {
	    this.#workspace = options.workspace, this.#button = options.button, this.#readScrollTop = options.readScrollTop, this.#readViewportHeight = options.readViewportHeight, this.#scrollToTop = options.scrollToTop, this.#now = options.now ?? (() => performance.now()), this.#setTimeout = options.setTimeout ?? ((callback, milliseconds) => window.setTimeout(callback, milliseconds)), this.#clearTimeout = options.clearTimeout ?? ((id) => window.clearTimeout(id)), this.#setInterval = options.setInterval ?? ((callback, milliseconds) => window.setInterval(callback, milliseconds)), this.#clearInterval = options.clearInterval ?? ((id) => window.clearInterval(id)), this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#button.hidden = !0, this.#workspace.changes.subscribe(() => this.#syncActivation(), this.scope);
	    for (const type of ["pointermove", "wheel"])
	      this.scope.listen(options.pointerTarget, type, (event) => {
	        this.#rememberPointer(event);
	      }, { passive: !0 });
	    this.scope.listen(options.scrollTarget, "scroll", () => this.#onScroll(), {
	      passive: !0
	    }), this.scope.listen(this.#button, "click", (event) => this.#onClick(event)), this.scope.add(() => this.#deactivate()), this.#syncActivation();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #syncActivation() {
	    if (this.#destroyed) return;
	    const active = this.#workspace.snapshot.presentation.embedded;
	    active !== this.#active && (active ? (this.#active = !0, this.#resetUpward(this.#safeScrollTop())) : this.#deactivate());
	  }
	  #deactivate() {
	    this.#active = !1, this.#hide(), this.#jumpFrame && this.#cancelFrame(this.#jumpFrame), this.#jumpFrame = 0, this.#jumping = !1, this.#pointerX = Number.NaN, this.#pointerY = Number.NaN, this.#resetUpward(0);
	  }
	  #safeScrollTop() {
	    return Math.max(0, Number(this.#readScrollTop()) || 0);
	  }
	  #hostBounds() {
	    const snapshot = this.#workspace.snapshot;
	    return snapshot.presentation.side === "left" ? { left: snapshot.embedWidth, right: snapshot.viewportWidth } : { left: 0, right: snapshot.viewportWidth - snapshot.embedWidth };
	  }
	  #rememberPointer(event) {
	    if (!this.#active || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY))
	      return;
	    const bounds = this.#hostBounds();
	    event.clientX < bounds.left || event.clientX > bounds.right || (this.#pointerX = event.clientX, this.#pointerY = event.clientY);
	  }
	  #onScroll() {
	    if (!this.#active) return;
	    const scrollTop = this.#safeScrollTop();
	    if (this.#jumping) {
	      this.#resetUpward(scrollTop);
	      return;
	    }
	    const now = this.#now(), upwardDelta = this.#scrollTop - scrollTop;
	    if (this.#scrollTop = scrollTop, scrollTop <= TOP_EDGE && this.#hide(), upwardDelta <= 0) {
	      this.#upwardDistance = 0, this.#upwardStartedAt = 0, this.#lastScrollAt = now;
	      return;
	    }
	    (!this.#upwardStartedAt || now - this.#lastScrollAt > UPWARD_BREAK_MS) && (this.#upwardStartedAt = now, this.#upwardDistance = 0), this.#lastScrollAt = now, this.#upwardDistance += upwardDelta, this.#upwardDistance >= UPWARD_DISTANCE && now - this.#upwardStartedAt <= UPWARD_WINDOW_MS && (this.#show(), this.#upwardStartedAt = now, this.#upwardDistance = 0);
	  }
	  #show() {
	    if (!this.#active || !Number.isFinite(this.#pointerX) || !Number.isFinite(this.#pointerY))
	      return;
	    if (this.#safeScrollTop() <= TOP_EDGE) {
	      this.#hide();
	      return;
	    }
	    if (this.#button.hidden) {
	      const bounds = this.#hostBounds(), left = Math.max(
	        bounds.left + TOP_EDGE,
	        Math.min(
	          bounds.right - BUTTON_SIZE - TOP_EDGE,
	          this.#pointerX + POINTER_GAP
	        )
	      ), top = Math.max(
	        TOP_EDGE,
	        Math.min(
	          this.#readViewportHeight() - BUTTON_SIZE - TOP_EDGE,
	          this.#pointerY - BUTTON_SIZE / 2
	        )
	      );
	      this.#button.style.left = `${Math.round(left)}px`, this.#button.style.top = `${Math.round(top)}px`, this.#button.hidden = !1;
	    }
	    this.#clearTimers();
	    const countdown = this.#button.querySelector(
	      ".ldp-reader-host-top-countdown,[data-reader-host-top-countdown]"
	    ), hideAt = this.#now() + SHOW_DURATION_MS, updateCountdown = () => {
	      countdown && (countdown.textContent = String(Math.max(
	        1,
	        Math.ceil((hideAt - this.#now()) / 1e3)
	      )));
	    };
	    updateCountdown(), this.#countdownTimer = this.#setInterval(updateCountdown, 200), this.#hideTimer = this.#setTimeout(() => this.#hide(), SHOW_DURATION_MS);
	  }
	  #hide() {
	    this.#clearTimers(), this.#button.hidden = !0;
	  }
	  #clearTimers() {
	    this.#hideTimer && this.#clearTimeout(this.#hideTimer), this.#countdownTimer && this.#clearInterval(this.#countdownTimer), this.#hideTimer = 0, this.#countdownTimer = 0;
	  }
	  #resetUpward(scrollTop) {
	    this.#scrollTop = Math.max(0, scrollTop), this.#upwardDistance = 0, this.#upwardStartedAt = 0, this.#lastScrollAt = 0;
	  }
	  #onClick(event) {
	    this.#active && (event.preventDefault(), event.stopPropagation(), this.#hide(), this.#jumping = !0, this.#scrollToTop(), this.#jumpFrame && this.#cancelFrame(this.#jumpFrame), this.#jumpFrame = this.#requestFrame(() => {
	      this.#jumpFrame = 0, this.#jumping = !1, this.#resetUpward(0);
	    }));
	  }
	}
}, "e5f43df781c7a5f30f18d8048664857a0badfe6b1f7a8e1121dbfcb17dd5c62f");

/* Source: lite/src/shell/embedded-host-topic-card-enhancement.ts */
runtime.register("src/shell/embedded-host-topic-card-enhancement.js", function(module, exports, require) {
	var embedded_host_topic_card_enhancement_exports = {};
	__export(embedded_host_topic_card_enhancement_exports, {
	  EmbeddedHostTopicCardEnhancement: () => EmbeddedHostTopicCardEnhancement
	});
	module.exports = __toCommonJS(embedded_host_topic_card_enhancement_exports);
	var import_value_record = require("../kernel/value-record.js");
	const CARD_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item", TOPIC_LINK_SELECTOR = 'a.raw-topic-link[href*="/t/"],a.title[href*="/t/"],a[href*="/t/"]', NEW_TOPIC_BADGE_SELECTOR = ".topic-post-badges,.badge-notification.new-topic", OPENED_TOPIC_STORAGE_KEY = "linuxdo-enhanced-reader:opened-host-topics:v1", MAX_OPENED_TOPIC_IDS = 2048;
	function modelValue(value, key) {
	  const source = (0, import_value_record.valueRecord)(value);
	  if (!source) return;
	  const getter = source.get;
	  if (typeof getter == "function")
	    try {
	      const result = getter.call(value, key);
	      if (result !== void 0) return result;
	    } catch {
	    }
	  return source[key];
	}
	function modelArray(value) {
	  if (Array.isArray(value)) return value;
	  const toArray = (0, import_value_record.valueRecord)(value)?.toArray;
	  if (typeof toArray != "function") return Object.freeze([]);
	  try {
	    const result = toArray.call(value);
	    return Array.isArray(result) ? result : Object.freeze([]);
	  } catch {
	    return Object.freeze([]);
	  }
	}
	function reactionCountTotal(value) {
	  const reactions = modelValue(value, "reactions"), source = (0, import_value_record.valueRecord)(reactions);
	  return !Array.isArray(reactions) && typeof source?.toArray != "function" ? null : modelArray(reactions).reduce((total, reaction) => {
	    const count = Number(modelValue(reaction, "count"));
	    return Number.isFinite(count) && count > 0 ? total + Math.trunc(count) : total;
	  }, 0);
	}
	function markup(node) {
	  return node.nodeType === 1 ? node.outerHTML : String(node.textContent ?? "");
	}
	function sourceNodes(cell, owned) {
	  return Object.freeze(
	    [...cell.childNodes].filter((node) => node !== owned).map((node) => node.cloneNode(!0))
	  );
	}
	function directChild(line, node) {
	  let current = node;
	  for (; current && current.parentElement !== line; )
	    current = current.parentElement;
	  return current?.parentElement === line ? current : null;
	}
	function normalizedLabel(value) {
	  return String(value ?? "").replace(/\s+/g, " ").trim();
	}
	function nativeDndLabel(node) {
	  return [
	    node.textContent,
	    node.getAttribute("aria-label"),
	    node.getAttribute("title"),
	    node.getAttribute("data-tooltip"),
	    node.getAttribute("data-tippy-content")
	  ].map(normalizedLabel).filter(Boolean).some(
	    (label) => label.includes("免打扰") || label.includes("静音") || /\b(?:mute|muted|unmute)\b/i.test(label)
	  );
	}
	class EmbeddedHostTopicCardEnhancement {
	  #document;
	  #host;
	  #isTopicHidden;
	  #hideTopic;
	  #automaticFilter;
	  #notify;
	  #onError;
	  #openedTopicStorage;
	  #openedTopicStorageKey;
	  #roots = /* @__PURE__ */ new Set();
	  #openedTopicIds = /* @__PURE__ */ new Set();
	  #rootClickHandlers = /* @__PURE__ */ new Map();
	  #pendingCards = /* @__PURE__ */ new WeakSet();
	  #nativeDndTooltipAttributes = /* @__PURE__ */ new WeakMap();
	  constructor(document, host, options = {}) {
	    this.#document = document, this.#host = host, this.#isTopicHidden = options.isTopicHidden ?? (() => !1), this.#hideTopic = options.hideTopic ?? (() => {
	      throw new Error("不想看仓库尚未就绪");
	    }), this.#automaticFilter = options.automaticFilter ?? (() => null), this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.#openedTopicStorage = options.openedTopicStorage ?? null;
	    const storageScope = normalizedLabel(
	      options.openedTopicStorageScope ?? this.#currentUsername()
	    ).toLocaleLowerCase("en-US") || "anonymous";
	    this.#openedTopicStorageKey = `${OPENED_TOPIC_STORAGE_KEY}:${encodeURIComponent(storageScope)}`, this.#restoreOpenedTopicIds();
	  }
	  syncRoot(root, mode = "embedded") {
	    if (this.#roots.add(root), !this.#rootClickHandlers.has(root)) {
	      const handler = (event) => this.#onRootClick(event);
	      root.addEventListener("click", handler, !0), this.#rootClickHandlers.set(root, handler);
	    }
	    this.syncCards(
	      Object.freeze([...root.querySelectorAll(CARD_SELECTOR)]),
	      mode
	    );
	  }
	  releaseRoot(root) {
	    this.#roots.has(root) && (this.#clearRoot(root), this.#roots.delete(root));
	  }
	  syncActivity(card) {
	    const source = card.querySelector(":scope > td.activity .relative-date"), component = card.querySelector(
	      ":scope > td.posts > .ldp-topic-stats-component"
	    ), target = component?.querySelector(
	      ".ldp-topic-stat--activity .relative-date"
	    );
	    if (!source || !component || !target) return !1;
	    target.textContent !== source.textContent && (target.textContent = source.textContent);
	    const parts = String(
	      component.dataset.ldpSourceSignature ?? ""
	    ).split(""), activity = card.querySelector(":scope > td.activity");
	    return parts.length === 5 && activity && (parts[2] = [...activity.childNodes].map(markup).join(""), component.dataset.ldpSourceSignature = parts.join("")), !0;
	  }
	  syncCards(cards, mode = "embedded") {
	    const topicModels = this.#topicModels(), reactions = this.#reactionCounts(topicModels);
	    for (const card of cards) {
	      if (!card.matches(CARD_SELECTOR) || card.closest(".ldp-overlay")) continue;
	      const topic = this.#topicInput(card, topicModels), projection = this.#topicProjection(card, topic);
	      if (this.#isTopicHidden(projection.topicId)) {
	        card.remove();
	        continue;
	      }
	      const automatic = card.dataset.ldpUnwantedFilterFailed === "true" ? null : this.#automaticFilter(projection);
	      if (automatic) {
	        this.#hideCard(card, projection, automatic, !1);
	        continue;
	      }
	      this.#markNewTopic(card, topic), this.#groupTitleTools(card, topic), mode === "embedded" ? (this.#markDateCells(card), this.#syncStats(card, reactions)) : this.#clearEmbeddedCard(card);
	    }
	  }
	  clear() {
	    for (const root of this.#roots)
	      this.#clearRoot(root);
	    this.#roots.clear();
	  }
	  markTopicOpened(topicId) {
	    if (!Number.isSafeInteger(topicId) || topicId < 1) return !1;
	    this.#openedTopicIds.has(topicId) || (this.#openedTopicIds.add(topicId), this.#persistOpenedTopicIds());
	    let changed = !1;
	    for (const card of this.#document.querySelectorAll(
	      CARD_SELECTOR
	    )) {
	      if (Number(this.#cardTopicId(card)) !== topicId) continue;
	      const marker = card.querySelector(NEW_TOPIC_BADGE_SELECTOR);
	      marker && !marker.hasAttribute("data-ldp-native-new-topic-marker") && (marker.setAttribute("data-ldp-native-new-topic-marker", "true"), changed = !0), card.hasAttribute("data-ldp-native-new-topic") && (card.removeAttribute("data-ldp-native-new-topic"), changed = !0);
	    }
	    return changed;
	  }
	  #clearRoot(root) {
	    const handler = this.#rootClickHandlers.get(root);
	    handler && root.removeEventListener("click", handler, !0), this.#rootClickHandlers.delete(root);
	    for (const component of root.querySelectorAll(
	      ".ldp-topic-stats-component"
	    )) component.remove();
	    for (const button of root.querySelectorAll(
	      "[data-ldp-owned-native-dnd]"
	    )) button.remove();
	    for (const group of root.querySelectorAll(
	      ".ldp-native-topic-title-tools"
	    )) group.replaceWith(...group.childNodes);
	    for (const node of root.querySelectorAll(
	      "[data-ldp-native-dnd]"
	    ))
	      this.#restoreNativeDndTooltip(node), node.removeAttribute("data-ldp-native-dnd");
	    for (const node of root.querySelectorAll(
	      "[data-ldp-native-topic-date]"
	    )) node.removeAttribute("data-ldp-native-topic-date");
	    for (const node of root.querySelectorAll(
	      "[data-ldp-native-old-topic]"
	    )) node.removeAttribute("data-ldp-native-old-topic");
	    for (const node of root.querySelectorAll(
	      "[data-ldp-native-topic-date-row],[data-ldp-native-dnd-ready],[data-ldp-native-new-topic],[data-ldp-native-new-topic-marker]"
	    ))
	      node.removeAttribute("data-ldp-native-topic-date-row"), node.removeAttribute("data-ldp-native-dnd-ready"), node.removeAttribute("data-ldp-native-new-topic"), node.removeAttribute("data-ldp-native-new-topic-marker");
	    for (const node of root.querySelectorAll(
	      "[data-ldp-topic-stats]"
	    )) node.removeAttribute("data-ldp-topic-stats");
	  }
	  #clearEmbeddedCard(card) {
	    card.querySelector(":scope > td.posts > .ldp-topic-stats-component")?.remove(), card.removeAttribute("data-ldp-topic-stats"), card.removeAttribute("data-ldp-native-topic-date-row");
	    for (const cell of card.querySelectorAll(
	      "[data-ldp-native-topic-date],[data-ldp-native-old-topic]"
	    ))
	      cell.removeAttribute("data-ldp-native-topic-date"), cell.removeAttribute("data-ldp-native-old-topic");
	  }
	  #syncStats(card, reactions) {
	    const posts = card.querySelector(":scope > td.posts"), views = card.querySelector(":scope > td.views"), activity = card.querySelector(":scope > td.activity");
	    if (!posts || !views || !activity) {
	      card.removeAttribute("data-ldp-topic-stats"), card.querySelector(":scope > td.posts > .ldp-topic-stats-component")?.remove();
	      return;
	    }
	    card.setAttribute("data-ldp-topic-stats", "true");
	    let component = posts.querySelector(
	      ":scope > .ldp-topic-stats-component"
	    );
	    const topicId = this.#cardTopicId(card), responseCount = topicId && reactions.has(topicId) ? reactions.get(topicId) : null, sourceSignature = [
	      [...posts.childNodes].filter((node) => node !== component).map(markup).join(""),
	      [...views.childNodes].map(markup).join(""),
	      [...activity.childNodes].map(markup).join(""),
	      views.dataset.ldpNativeOldTopic === "true" ? "old" : "recent",
	      responseCount ?? ""
	    ].join("");
	    if (component?.dataset.ldpSourceSignature === sourceSignature && component.querySelectorAll(":scope > .ldp-topic-stats-row").length === 2) return;
	    component || (component = this.#document.createElement("div"), component.className = "ldp-topic-stats-component", posts.append(component));
	    const stat = (kind, label, nodes) => {
	      const item = this.#document.createElement("span");
	      item.className = `ldp-topic-stat ldp-topic-stat--${kind}`;
	      const caption = this.#document.createElement("span");
	      caption.className = "ldp-topic-stat-label", caption.textContent = label;
	      const value = this.#document.createElement("span");
	      return value.className = "ldp-topic-stat-value", value.append(...nodes), item.append(caption, value), item;
	    }, row = (...items) => {
	      const value = this.#document.createElement("span");
	      return value.className = "ldp-topic-stats-row", value.append(...items), value;
	    }, oldTopic = views.dataset.ldpNativeOldTopic === "true";
	    component.replaceChildren(
	      row(
	        stat("reply", "回复", sourceNodes(posts, component)),
	        stat(
	          oldTopic ? "old" : "date",
	          oldTopic ? "旧帖" : "最近回复",
	          sourceNodes(views, null)
	        )
	      ),
	      row(
	        stat("activity", "活跃", sourceNodes(activity, null)),
	        stat(
	          "response",
	          "回应",
	          [this.#document.createTextNode(
	            responseCount == null ? "—" : String(responseCount)
	          )]
	        )
	      )
	    ), component.dataset.ldpSourceSignature = sourceSignature;
	  }
	  #markDateCells(card) {
	    const cells = [...card.querySelectorAll(":scope > td")], dateCell = card.querySelector(":scope > td.views") ?? cells[3] ?? null;
	    let hasDate = !1;
	    for (const cell of cells) {
	      const text = cell === dateCell ? String(cell.textContent ?? "").replace(/\s+/g, " ").trim() : "", date = /\b(?:\d{4}[/-])?\d{1,2}[/-]\d{1,2}\s+\d{1,2}:\d{2}\b/.test(text), old = /\b\d{4}[/-]\d{1,2}[/-]\d{1,2}\s+\d{1,2}:\d{2}\b/.test(text);
	      hasDate = hasDate || date, cell.toggleAttribute("data-ldp-native-topic-date", date), cell.toggleAttribute("data-ldp-native-old-topic", old);
	    }
	    card.toggleAttribute("data-ldp-native-topic-date-row", hasDate);
	  }
	  #markNewTopic(card, topic) {
	    const previous = card.querySelector(
	      "[data-ldp-native-new-topic-marker]"
	    ), legacyMarker = card.querySelector(
	      ".badge-notification.new-topic"
	    ), unseen = modelValue(topic, "unseen"), hostNew = unseen === !0 || unseen !== !1 && !!(legacyMarker ?? previous), topicId = Number(this.#cardTopicId(card));
	    !hostNew && Number.isSafeInteger(topicId) && this.#openedTopicIds.delete(topicId) && this.#persistOpenedTopicIds();
	    const marker = hostNew ? card.querySelector(NEW_TOPIC_BADGE_SELECTOR) : null;
	    previous && previous !== marker && previous.removeAttribute("data-ldp-native-new-topic-marker"), marker?.setAttribute("data-ldp-native-new-topic-marker", "true"), card.toggleAttribute(
	      "data-ldp-native-new-topic",
	      hostNew && !this.#openedTopicIds.has(topicId)
	    );
	  }
	  #restoreOpenedTopicIds() {
	    if (!this.#openedTopicStorage) return;
	    let raw;
	    try {
	      raw = this.#openedTopicStorage.getItem(this.#openedTopicStorageKey);
	    } catch (cause) {
	      this.#onError(cause);
	      return;
	    }
	    if (raw)
	      try {
	        const parsed = JSON.parse(raw);
	        if (!Array.isArray(parsed)) throw new TypeError("本机已打开 Topic 记录格式无效");
	        for (const value of parsed.slice(-MAX_OPENED_TOPIC_IDS)) {
	          const topicId = Number(value);
	          Number.isSafeInteger(topicId) && topicId > 0 && this.#openedTopicIds.add(topicId);
	        }
	      } catch (cause) {
	        this.#onError(cause);
	        try {
	          this.#openedTopicStorage.removeItem(this.#openedTopicStorageKey);
	        } catch (removeCause) {
	          this.#onError(removeCause);
	        }
	      }
	  }
	  #persistOpenedTopicIds() {
	    if (this.#openedTopicStorage) {
	      for (; this.#openedTopicIds.size > MAX_OPENED_TOPIC_IDS; ) {
	        const oldest = this.#openedTopicIds.values().next().value;
	        if (oldest === void 0) break;
	        this.#openedTopicIds.delete(oldest);
	      }
	      try {
	        if (!this.#openedTopicIds.size) {
	          this.#openedTopicStorage.removeItem(this.#openedTopicStorageKey);
	          return;
	        }
	        this.#openedTopicStorage.setItem(
	          this.#openedTopicStorageKey,
	          JSON.stringify([...this.#openedTopicIds])
	        );
	      } catch (cause) {
	        this.#onError(cause);
	      }
	    }
	  }
	  #groupTitleTools(card, topic) {
	    const line = card.querySelector(".link-top-line");
	    if (!line) return;
	    const direct = [...line.querySelectorAll(
	      'a,button,[role="button"]'
	    )].find(
	      (node) => !node.matches(TOPIC_LINK_SELECTOR) && nativeDndLabel(node)
	    ), fallback = direct ? null : [...line.querySelectorAll("span")].find(
	      (node) => !node.closest(TOPIC_LINK_SELECTOR) && nativeDndLabel(node)
	    );
	    let dnd = direct ?? fallback?.closest(
	      'a,button,[role="button"]'
	    ) ?? fallback;
	    if (!dnd && topic && this.#currentUsername() && (dnd = this.#createDndButton(line, topic)), !dnd) return;
	    dnd.dataset.ldpNativeDnd = "true", line.dataset.ldpNativeDndReady = "true", this.#prepareDndTooltip(dnd);
	    const expert = [...line.querySelectorAll("*")].find((node) => normalizedLabel(node.textContent) === "专家回应");
	    if (!expert) return;
	    const startNode = directChild(line, expert), endNode = directChild(line, dnd);
	    if (!startNode || !endNode || startNode === endNode && startNode.classList.contains("ldp-native-topic-title-tools")) return;
	    const children = [...line.children], start = children.indexOf(startNode), end = children.indexOf(endNode);
	    if (start < 0 || end < start) return;
	    const group = this.#document.createElement("span");
	    group.className = "ldp-native-topic-title-tools", startNode.before(group), group.append(...children.slice(start, end + 1));
	  }
	  #createDndButton(line, _topic) {
	    const button = this.#document.createElement("button");
	    return button.type = "button", button.dataset.ldpOwnedNativeDnd = "true", button.dataset.ldpNativeDnd = "true", button.setAttribute("aria-label", "免打扰:加入不想看"), line.append(button), button;
	  }
	  #onRootClick(event) {
	    const eventTarget = event.target, control = (eventTarget?.nodeType === 1 ? eventTarget : eventTarget?.parentElement ?? null)?.closest("[data-ldp-native-dnd]"), card = control?.closest(CARD_SELECTOR);
	    if (!control || !card || (event.preventDefault(), event.stopImmediatePropagation(), this.#pendingCards.has(card))) return;
	    const topic = this.#topicInput(card, this.#topicModels()), projection = this.#topicProjection(card, topic);
	    this.#hideCard(card, projection, null, !0, control);
	  }
	  async #hideCard(card, input, match, manual, control) {
	    if (!this.#pendingCards.has(card)) {
	      this.#pendingCards.add(card), control && (control.dataset.ldpNativeDndPending = "true", control.setAttribute("aria-busy", "true"), control.tagName === "BUTTON" && (control.disabled = !0));
	      try {
	        await this.#hideTopic(Object.freeze({
	          topicId: input.topicId,
	          title: input.title,
	          href: input.href,
	          categoryId: input.categoryId,
	          categoryName: input.categoryName,
	          categorySlug: input.categorySlug,
	          source: manual ? "manual" : "automatic",
	          matchedRule: match?.label ?? "",
	          matchedCategory: match?.matches.some((reason) => reason.kind === "category") ?? !1
	        })), card.remove(), manual && this.#notify("已加入不想看");
	      } catch (cause) {
	        this.#onError(cause), card.dataset.ldpUnwantedFilterFailed = "true", this.#notify(manual ? "加入不想看失败,请稍后重试" : "自动免打扰保存失败,请稍后重试");
	      } finally {
	        this.#pendingCards.delete(card), control?.isConnected && (delete control.dataset.ldpNativeDndPending, control.removeAttribute("aria-busy"), control.tagName === "BUTTON" && (control.disabled = !1));
	      }
	    }
	  }
	  #prepareDndTooltip(control) {
	    const attributes = [
	      "aria-label",
	      "title",
	      "data-tooltip",
	      "data-tippy-content",
	      "data-ldp-tooltip-label"
	    ];
	    control.dataset.ldpOwnedNativeDnd !== "true" && !this.#nativeDndTooltipAttributes.has(control) && this.#nativeDndTooltipAttributes.set(control, new Map(
	      attributes.map((name) => [
	        name,
	        control.getAttribute(name)
	      ])
	    )), control.setAttribute("aria-label", "免打扰:加入不想看");
	    for (const name of attributes.slice(1)) control.removeAttribute(name);
	  }
	  #restoreNativeDndTooltip(control) {
	    delete control.dataset.ldpTooltipLabel;
	    const attributes = this.#nativeDndTooltipAttributes.get(control);
	    if (attributes) {
	      for (const [name, value] of attributes)
	        value === null ? control.removeAttribute(name) : control.setAttribute(name, value);
	      this.#nativeDndTooltipAttributes.delete(control);
	    }
	  }
	  #currentUsername() {
	    return normalizedLabel(modelValue(
	      this.#host.lookup("service:current-user"),
	      "username"
	    ));
	  }
	  #topicModels() {
	    const result = /* @__PURE__ */ new Map(), router = this.#host.lookup("service:router"), routeName = String(
	      modelValue(router, "currentRouteName") ?? ""
	    ).trim(), controllerNames = [
	      routeName ? `controller:${routeName}` : "",
	      routeName ? `controller:${routeName.replaceAll(".", "/")}` : "",
	      "controller:discovery/list",
	      "controller:tag/show",
	      "controller:tags/intersection"
	    ].filter(Boolean), seen = /* @__PURE__ */ new Set();
	    for (const name of controllerNames) {
	      const controller = this.#host.lookup(name);
	      if (!controller || seen.has(controller)) continue;
	      seen.add(controller);
	      const model = modelValue(controller, "model"), list = modelValue(model, "list") ?? modelValue(controller, "list") ?? model, topics = modelArray(
	        modelValue(list, "topics") ?? modelValue(list, "content")
	      );
	      for (const topic of topics) {
	        const topicId = String(modelValue(topic, "id") ?? "").trim();
	        !topicId || result.has(topicId) || result.set(topicId, topic);
	      }
	    }
	    return result;
	  }
	  #reactionCounts(topics) {
	    const counts = /* @__PURE__ */ new Map();
	    for (const [topicId, topic] of topics) {
	      const reactions = modelValue(topic, "op_reactions_data") ?? modelValue(topic, "opReactionsData"), total = reactionCountTotal(reactions), raw = reactions ? total ?? modelValue(reactions, "reaction_users_count") ?? modelValue(reactions, "reactionUsersCount") : modelValue(topic, "op_like_count") ?? modelValue(topic, "opLikeCount"), numeric = Number(raw);
	      counts.set(
	        topicId,
	        Number.isFinite(numeric) && numeric >= 0 ? Math.trunc(numeric) : null
	      );
	    }
	    return counts;
	  }
	  #topicInput(card, topics) {
	    const topicId = this.#cardTopicId(card);
	    if (!topicId) return null;
	    const model = topics.get(topicId);
	    if (model) return model;
	    const numericId = Number(topicId);
	    if (!Number.isSafeInteger(numericId) || numericId < 1) return null;
	    const link = card.querySelector(TOPIC_LINK_SELECTOR);
	    return Object.freeze({
	      id: numericId,
	      title: normalizedLabel(link?.textContent),
	      slug: "topic",
	      notification_level: 1
	    });
	  }
	  #topicProjection(card, topic) {
	    const topicId = Number(this.#cardTopicId(card));
	    if (!Number.isSafeInteger(topicId) || topicId < 1)
	      throw new Error("宿主 Topic 卡片缺少有效 topic.id");
	    const link = card.querySelector(TOPIC_LINK_SELECTOR), category = modelValue(topic, "category"), categoryIdRaw = Number(
	      modelValue(topic, "category_id") ?? modelValue(category, "id")
	    ), categoryName = normalizedLabel(
	      modelValue(category, "name") ?? modelValue(topic, "category_name") ?? card.querySelector(".category-name,.badge-category__name")?.textContent
	    ), categorySlug = normalizedLabel(
	      modelValue(category, "slug") ?? modelValue(topic, "category_slug")
	    ), rawLabels = modelArray(
	      modelValue(topic, "tags") ?? modelValue(topic, "topic_tags")
	    ), labels = /* @__PURE__ */ new Map(), rememberLabel = (value) => {
	      const label = normalizedLabel(
	        typeof value == "string" ? value : modelValue(value, "name") ?? modelValue(value, "id") ?? modelValue(value, "slug")
	      ), key = label.toLocaleLowerCase("zh-CN");
	      key && !labels.has(key) && labels.set(key, label);
	    };
	    for (const label of rawLabels) rememberLabel(label);
	    for (const label of card.querySelectorAll(
	      ".discourse-tag,[data-tag-name],.list-tags a"
	    ))
	      rememberLabel(label.dataset.tagName ?? label.textContent);
	    const creator = modelValue(topic, "creator"), posters = modelArray(modelValue(topic, "posters")), opPoster = posters.find((poster) => {
	      const description = normalizedLabel(modelValue(poster, "description"));
	      return /\b(?:original poster|op)\b|楼主|发帖人/i.test(description) || modelValue(poster, "original_poster") === !0;
	    }) ?? posters[0], domPoster = card.querySelector(
	      ".posters a[data-user-card].original-poster,.posters a[data-user-card]:first-child,.posters [data-user-card]:first-child"
	    ), authorUsername = normalizedLabel(
	      modelValue(creator, "username") ?? modelValue(topic, "creator_username") ?? modelValue(opPoster, "username") ?? domPoster?.dataset.userCard
	    ).replace(/^@+/, "");
	    return Object.freeze({
	      topicId,
	      title: normalizedLabel(
	        modelValue(topic, "title") ?? link?.textContent
	      ) || `帖子 #${topicId}`,
	      href: link?.getAttribute("href") ?? `/t/${topicId}`,
	      categoryId: Number.isSafeInteger(categoryIdRaw) && categoryIdRaw > 0 ? categoryIdRaw : null,
	      categoryName,
	      categorySlug,
	      labels: Object.freeze([...labels.values()]),
	      authorUsername
	    });
	  }
	  #cardTopicId(card) {
	    return String(
	      card.dataset.topicId ?? card.getAttribute("data-topic-id") ?? this.#topicId(card.querySelector(TOPIC_LINK_SELECTOR))
	    ).trim();
	  }
	  #topicId(link) {
	    return (link?.getAttribute("href") ?? "").match(/\/t\/(?:[^/]+\/)?(\d+)(?:\/|$)/)?.[1] ?? "";
	  }
	}
}, "f8481d0a58620cbb850b9697399af84e685ac5fcb8f31b7d7e2948f3113e4be7");

/* Source: lite/src/shell/main-outlet-mutation-hub.ts */
runtime.register("src/shell/main-outlet-mutation-hub.js", function(module, exports, require) {
	var main_outlet_mutation_hub_exports = {};
	__export(main_outlet_mutation_hub_exports, {
	  MainOutletMutationHub: () => MainOutletMutationHub
	});
	module.exports = __toCommonJS(main_outlet_mutation_hub_exports);
	const MAIN_OUTLET_SELECTORS = Object.freeze([
	  "#main-outlet",
	  ".list-container,.topic-list,.latest-topic-list",
	  "#ember-app"
	]);
	function mainOutlet(documentPort) {
	  for (const selector of MAIN_OUTLET_SELECTORS) {
	    const root = documentPort.querySelector(selector);
	    if (root) return root;
	  }
	  return null;
	}
	class MainOutletMutationHub {
	  #document;
	  #createObserver;
	  #onListenerError;
	  #listeners = /* @__PURE__ */ new Set();
	  #observer = null;
	  #root = null;
	  #destroyed = !1;
	  constructor(options) {
	    this.#document = options.document, this.#createObserver = options.createObserver ?? ((callback) => new MutationObserver(callback)), this.#onListenerError = options.onListenerError ?? (() => {
	    });
	  }
	  get currentRoot() {
	    return this.#root;
	  }
	  subscribe(listener, scope) {
	    if (this.#destroyed) throw new Error("MainOutletMutationHub 已销毁");
	    this.#listeners.add(listener), this.#listeners.size === 1 && this.#start(), this.#notifyOne(listener, Object.freeze({
	      root: this.#root,
	      rootChanged: !0,
	      records: Object.freeze([])
	    }));
	    let active = !0;
	    const unsubscribe = () => {
	      active && (active = !1, this.#listeners.delete(listener), this.#listeners.size || this.#stop());
	    };
	    return scope?.add(unsubscribe), unsubscribe;
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.#listeners.clear(), this.#stop());
	  }
	  #start() {
	    this.#observer || !this.#document.body || (this.#observer = this.#createObserver((records) => this.#receive(records)), this.#retarget());
	  }
	  #stop() {
	    this.#observer?.disconnect(), this.#observer = null, this.#root = null;
	  }
	  #retarget() {
	    const observer = this.#observer, body = this.#document.body;
	    if (!observer || !body) return !1;
	    const nextRoot = mainOutlet(this.#document);
	    if (this.#root === nextRoot && nextRoot?.isConnected) return !1;
	    if (observer.disconnect(), observer.observe(body, { childList: !0 }), nextRoot) {
	      let ancestor = nextRoot.parentElement;
	      for (; ancestor && ancestor !== body; )
	        observer.observe(ancestor, { childList: !0 }), ancestor = ancestor.parentElement;
	      observer.observe(nextRoot, {
	        childList: !0,
	        subtree: !0,
	        characterData: !0
	      });
	    }
	    return this.#root = nextRoot, !0;
	  }
	  #receive(records) {
	    if (this.#destroyed || !this.#listeners.size) return;
	    const body = this.#document.body, rootChanged = records.some(
	      (record) => record.target === body || !this.#root?.isConnected || !this.#root.contains(record.target)
	    ) ? this.#retarget() : !1, batch = Object.freeze({
	      root: this.#root,
	      rootChanged,
	      records: Object.freeze([...records])
	    });
	    for (const listener of [...this.#listeners]) this.#notifyOne(listener, batch);
	  }
	  #notifyOne(listener, batch) {
	    try {
	      listener(batch);
	    } catch (error) {
	      this.#onListenerError(error);
	    }
	  }
	}
}, "1b77cf81270ca6322e13b2672695a5a2fcb0a16f057ebe5dc6b8817a3c54763c");

/* Source: lite/src/shell/reader-action-form-support.ts */
runtime.register("src/shell/reader-action-form-support.js", function(module, exports, require) {
	var reader_action_form_support_exports = {};
	__export(reader_action_form_support_exports, {
	  ReaderActionFormSession: () => ReaderActionFormSession,
	  ReaderActionFormSurfaceHost: () => ReaderActionFormSurfaceHost,
	  ReaderActionFormTiming: () => ReaderActionFormTiming,
	  createReaderActionFormFrame: () => createReaderActionFormFrame,
	  handleReaderActionDialogKeydown: () => handleReaderActionDialogKeydown,
	  renderReaderActionIcon: () => renderReaderActionIcon,
	  setReaderActionFormBusy: () => setReaderActionFormBusy
	});
	module.exports = __toCommonJS(reader_action_form_support_exports);
	var import_event_target = require("../dom/event-target.js"), import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_action_surface_coordinator = require("./reader-action-surface-coordinator.js");
	function renderReaderActionIcon(document, name, renderIcon) {
	  return (0, import_reader_icon.renderReaderIcon)(document, name, renderIcon);
	}
	function createReaderActionFormFrame(options) {
	  const { document } = options, layer = document.createElement("div");
	  layer.className = "ldp-reader-action-layer";
	  const dialog = document.createElement("section");
	  dialog.className = "ldp-reader-action-dialog", dialog.setAttribute("role", "dialog"), dialog.setAttribute("aria-modal", "true"), dialog.setAttribute("aria-labelledby", options.titleId);
	  const head = document.createElement("div");
	  head.className = "ldp-reader-action-head";
	  const title = document.createElement("strong");
	  title.id = options.titleId, title.textContent = options.title;
	  const close = document.createElement("button");
	  close.type = "button", close.className = "ldp-reader-action-close", close.setAttribute(options.closeDataAttribute, ""), close.setAttribute("aria-label", "关闭"), close.append(renderReaderActionIcon(document, "x", options.renderIcon)), head.append(title, close);
	  const form = document.createElement("form");
	  form.className = "ldp-reader-action-form";
	  const body = document.createElement("div");
	  if (body.className = "ldp-reader-action-body", options.intro) {
	    const intro = document.createElement("p");
	    intro.className = "ldp-reader-action-intro", intro.textContent = options.intro, body.append(intro);
	  }
	  const status = document.createElement("p");
	  status.className = "ldp-reader-action-status", status.setAttribute("role", "status"), status.setAttribute("aria-live", "polite"), body.append(status);
	  const footer = document.createElement("div");
	  footer.className = "ldp-reader-action-footer";
	  const cancel = document.createElement("button");
	  cancel.type = "button", cancel.className = "ldp-reader-action-cancel", cancel.setAttribute(options.cancelDataAttribute, ""), cancel.textContent = "取消";
	  const submit = document.createElement("button");
	  return submit.type = "submit", submit.className = "ldp-reader-action-submit", submit.textContent = options.submitLabel, footer.append(cancel, submit), form.append(body, footer), dialog.append(head, form), layer.append(dialog), { layer, dialog, form, body, status, cancel, submit };
	}
	class ReaderActionFormTiming {
	  #document;
	  #successDelayMs;
	  #schedule;
	  #cancel;
	  #focusSoon;
	  #closeTimer = null;
	  constructor(document, options) {
	    if (this.#document = document, this.#successDelayMs = Number(options.successDelayMs ?? 650), !Number.isFinite(this.#successDelayMs) || this.#successDelayMs < 0)
	      throw new RangeError("successDelayMs 必须是非负有限数值");
	    this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(
	      handle
	    )), this.#focusSoon = options.focusSoon ?? ((callback) => {
	      const view = this.#document.defaultView;
	      view?.requestAnimationFrame ? view.requestAnimationFrame(callback) : queueMicrotask(callback);
	    });
	  }
	  focus(callback) {
	    this.#focusSoon(callback);
	  }
	  restore(previousFocus) {
	    this.focus(() => {
	      previousFocus?.isConnected && typeof previousFocus.focus == "function" && previousFocus.focus({ preventScroll: !0 });
	    });
	  }
	  scheduleClose(callback) {
	    this.clear(), this.#closeTimer = this.#schedule(callback, this.#successDelayMs);
	  }
	  clear() {
	    this.#closeTimer !== null && (this.#cancel(this.#closeTimer), this.#closeTimer = null);
	  }
	}
	function setReaderActionFormBusy(form, submit, busy, busyLabel, idleLabel) {
	  for (const control of form.querySelectorAll("input,textarea,select,button"))
	    control.disabled = busy;
	  submit.textContent = busy ? busyLabel : idleLabel;
	}
	function handleReaderActionDialogKeydown(event, document, dialog, busy, close) {
	  if (event.key === "Escape" && !busy) {
	    event.preventDefault(), close();
	    return;
	  }
	  if (event.key !== "Tab") return;
	  const controls = [
	    ...dialog.querySelectorAll(
	      "input:not(:disabled),textarea:not(:disabled),select:not(:disabled),button:not(:disabled)"
	    )
	  ];
	  if (!controls.length) return;
	  const first = controls[0], last = controls.at(-1), active = (0, import_event_target.deepActiveElement)(document);
	  event.shiftKey && active === first ? (event.preventDefault(), last.focus()) : !event.shiftKey && active === last && (event.preventDefault(), first.focus());
	}
	class ReaderActionFormSurfaceHost {
	  scope;
	  document;
	  renderIcon;
	  #root;
	  #label;
	  #timing;
	  #coordinator;
	  #active = null;
	  #id = 0;
	  constructor(options) {
	    this.document = options.document, this.#root = options.root, this.#label = options.label, this.renderIcon = options.renderIcon, this.#timing = new ReaderActionFormTiming(options.document, options), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#coordinator = options.coordinator ?? new import_reader_action_surface_coordinator.ReaderActionSurfaceCoordinator({ parentScope: this.scope }), this.scope.add(() => this.close(!1));
	  }
	  prepare() {
	    this.#assertActive();
	    const previousFocus = this.#active?.previousFocus ?? (0, import_event_target.deepActiveElement)(this.document);
	    return this.close(!1), Object.freeze({
	      id: ++this.#id,
	      previousFocus
	    });
	  }
	  start(options) {
	    this.#assertActive();
	    let session;
	    return session = new ReaderActionFormSession({
	      document: this.document,
	      root: this.#root,
	      frame: options.frame,
	      timing: this.#timing,
	      coordinator: this.#coordinator,
	      previousFocus: options.previousFocus,
	      closeSelector: options.closeSelector,
	      cancelSelector: options.cancelSelector,
	      signal: options.signal,
	      onSettled: () => {
	        this.#active === session && (this.#active = null), options.onSettled?.();
	      }
	    }), this.#active = session, session;
	  }
	  close(submitted = !1) {
	    this.#active?.settle(submitted);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error(`${this.#label} 已销毁`);
	  }
	}
	class ReaderActionFormSession {
	  previousFocus;
	  result;
	  #options;
	  #resolve;
	  #releaseAction = () => {
	  };
	  #removeAbortListener = () => {
	  };
	  #busy = !1;
	  #mounted = !1;
	  #settled = !1;
	  constructor(options) {
	    this.#options = options, this.previousFocus = options.previousFocus, this.result = new Promise((resolve) => {
	      this.#resolve = resolve;
	    });
	  }
	  get active() {
	    return !this.#settled;
	  }
	  get busy() {
	    return this.#busy;
	  }
	  setBusy(busy) {
	    this.#settled || (this.#busy = busy);
	  }
	  mount(focus) {
	    if (this.#mounted || this.#settled) return;
	    this.#mounted = !0;
	    const { document, root, frame, coordinator, signal } = this.#options;
	    if (this.#releaseAction = coordinator.claim(() => this.settle(!1)), frame.layer.addEventListener("click", (event) => {
	      const target = event.target;
	      target?.closest(this.#options.closeSelector) ? this.settle(!1) : !this.#busy && (event.target === frame.layer || target?.closest(this.#options.cancelSelector)) && this.settle(!1);
	    }), frame.layer.addEventListener("keydown", (event) => {
	      handleReaderActionDialogKeydown(
	        event,
	        document,
	        frame.dialog,
	        this.#busy,
	        () => this.settle(!1)
	      );
	    }), frame.layer.addEventListener("wheel", (event) => (0, import_floating_surface_wheel.containFloatingSurfaceWheel)(frame.layer, event), {
	      passive: !1
	    }), root.append(frame.layer), signal) {
	      const onAbort = () => this.settle(!1);
	      signal.addEventListener("abort", onAbort, { once: !0 }), this.#removeAbortListener = () => signal.removeEventListener("abort", onAbort), signal.aborted && this.settle(!1);
	    }
	    this.#options.timing.focus(() => {
	      frame.layer.isConnected && focus();
	    });
	  }
	  resetStatus() {
	    this.#options.frame.status.classList.remove("success"), this.#options.frame.status.textContent = "";
	  }
	  submit(options) {
	    if (this.#busy || this.#settled) return;
	    this.setBusy(!0), options.setBusy(!0);
	    let pending;
	    try {
	      pending = options.execute();
	    } catch (cause) {
	      this.#rejectSubmission(cause, options);
	      return;
	    }
	    Promise.resolve(pending).then((value) => {
	      if (this.#settled) return;
	      const status = this.#options.frame.status;
	      status.classList.add("success"), status.textContent = options.successMessage(value), this.#options.timing.scheduleClose(() => this.settle(!0));
	    }).catch((cause) => this.#rejectSubmission(cause, options));
	  }
	  settle(submitted) {
	    this.#settled || (this.#settled = !0, this.#releaseAction(), this.#removeAbortListener(), this.#options.timing.clear(), this.#options.frame.layer.remove(), this.#options.onSettled?.(), this.#options.timing.restore(this.previousFocus), this.#resolve(submitted));
	  }
	  #rejectSubmission(cause, options) {
	    this.#settled || (this.#options.frame.status.textContent = options.failureMessage(cause), this.setBusy(!1), options.setBusy(!1));
	  }
	}
}, "6d9b160295ad415aa13e84b6cf19abf62ff4ef8e5b7ba6e4f83396376b87290e");

/* Source: lite/src/shell/reader-action-surface-coordinator.ts */
runtime.register("src/shell/reader-action-surface-coordinator.js", function(module, exports, require) {
	var reader_action_surface_coordinator_exports = {};
	__export(reader_action_surface_coordinator_exports, {
	  ReaderActionSurfaceCoordinator: () => ReaderActionSurfaceCoordinator
	});
	module.exports = __toCommonJS(reader_action_surface_coordinator_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderActionSurfaceCoordinator {
	  scope;
	  #active = null;
	  constructor(options = {}) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.closeActive();
	    });
	  }
	  get active() {
	    return this.#active !== null;
	  }
	  claim(close) {
	    if (this.scope.destroyed)
	      throw new Error("ReaderActionSurfaceCoordinator 已销毁");
	    const previous = this.#active;
	    this.#active = null, previous?.close();
	    const token = Symbol("reader-action-surface");
	    this.#active = { token, close };
	    let released = !1;
	    return () => {
	      released || (released = !0, this.#active?.token === token && (this.#active = null));
	    };
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  closeActive() {
	    const active = this.#active;
	    return this.#active = null, active?.close(), active !== null;
	  }
	}
}, "fe11b3d151a62bd97b52adbcfe525c6e29c3e75aa7df95756ea3520b0442456c");

/* Source: lite/src/shell/reader-assignment-form-surface.ts */
runtime.register("src/shell/reader-assignment-form-surface.js", function(module, exports, require) {
	var reader_assignment_form_surface_exports = {};
	__export(reader_assignment_form_surface_exports, {
	  ReaderAssignmentFormSurface: () => ReaderAssignmentFormSurface
	});
	module.exports = __toCommonJS(reader_assignment_form_surface_exports);
	var import_reader_action_form_support = require("./reader-action-form-support.js");
	class ReaderAssignmentFormSurface {
	  scope;
	  #host;
	  #users;
	  #scheduleLookup;
	  #cancelLookup;
	  constructor(options) {
	    this.#host = new import_reader_action_form_support.ReaderActionFormSurfaceHost({
	      ...options,
	      label: "ReaderAssignmentFormSurface"
	    }), this.scope = this.#host.scope, this.#users = options.users, this.#scheduleLookup = options.schedule ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs)), this.#cancelLookup = options.cancel ?? ((handle) => globalThis.clearTimeout(
	      handle
	    ));
	  }
	  open(request) {
	    if (request.signal?.aborted) return Promise.resolve(!1);
	    const { id, previousFocus } = this.#host.prepare(), document = this.#host.document, titleId = `ldp-reader-assignment-title-${id}`, frame = (0, import_reader_action_form_support.createReaderActionFormFrame)({
	      document,
	      titleId,
	      title: request.title,
	      intro: request.intro,
	      closeDataAttribute: "data-assignment-close",
	      cancelDataAttribute: "data-assignment-cancel",
	      submitLabel: "确认指定",
	      renderIcon: this.#host.renderIcon
	    });
	    frame.dialog.classList.add("ldp-reader-assignment-dialog");
	    const { form, status, submit } = frame, usernameField = document.createElement("label");
	    usernameField.className = "ldp-reader-action-field";
	    const usernameLookup = document.createElement("div");
	    usernameLookup.className = "ldp-reader-assignment-user-lookup";
	    const usernameLabel = document.createElement("span");
	    usernameLabel.textContent = "用户名";
	    const username = document.createElement("input");
	    username.name = "username", username.type = "text", username.maxLength = 100, username.autocomplete = "off", username.placeholder = "不含 @", username.required = !0, username.value = String(request.initialUsername ?? "").replace(/^@+/, ""), username.setAttribute("aria-describedby", `${titleId}-status`), username.setAttribute("aria-autocomplete", "list"), username.setAttribute("aria-controls", `${titleId}-candidates`), username.setAttribute("aria-expanded", "false"), status.id = `${titleId}-status`, usernameField.append(usernameLabel, username);
	    const candidates = document.createElement("div");
	    candidates.id = `${titleId}-candidates`, candidates.className = "ldp-reader-assignment-user-candidates", candidates.setAttribute("role", "listbox"), candidates.setAttribute("aria-label", "匹配的社区用户"), candidates.hidden = !0, usernameLookup.append(usernameField, candidates);
	    const noteField = document.createElement("label");
	    noteField.className = "ldp-reader-action-field";
	    const noteLabel = document.createElement("span");
	    noteLabel.textContent = "备注(选填)";
	    const note = document.createElement("textarea");
	    note.name = "note", note.maxLength = 1e3, note.placeholder = "说明这次指定的原因或任务", noteField.append(noteLabel, note), status.before(usernameLookup, noteField);
	    let lookupTimer = null, lookupSequence = 0, verifiedInput = "", verifiedUsername = "", candidateUsers = Object.freeze([]);
	    const cancelLookup = () => {
	      lookupSequence += 1, lookupTimer !== null && this.#cancelLookup(lookupTimer), lookupTimer = null;
	    }, session = this.#host.start({
	      frame,
	      previousFocus,
	      closeSelector: "[data-assignment-close]",
	      cancelSelector: "[data-assignment-cancel]",
	      signal: request.signal,
	      onSettled: cancelLookup
	    }), normalizeUsername = (value) => value.trim().replace(/^@+/, ""), usernameKey = (value) => normalizeUsername(value).toLocaleLowerCase("zh-CN"), showLookupStatus = (message, tone) => {
	      status.textContent = message, status.classList.toggle("success", tone === "success"), status.classList.toggle("is-neutral", tone === "neutral");
	    }, hideCandidates = () => {
	      candidates.hidden = !0, username.setAttribute("aria-expanded", "false");
	    }, clearCandidates = () => {
	      candidateUsers = Object.freeze([]), candidates.replaceChildren(), hideCandidates();
	    }, selectUser = (user) => {
	      const selectedUsername = normalizeUsername(user.username);
	      selectedUsername && (cancelLookup(), username.value = selectedUsername, verifiedInput = usernameKey(selectedUsername), verifiedUsername = selectedUsername, username.setAttribute("aria-invalid", "false"), submit.disabled = !1, clearCandidates(), showLookupStatus(`已选择 @${selectedUsername}。`, "success"), username.focus({ preventScroll: !0 }));
	    }, renderCandidates = (users) => {
	      candidateUsers = Object.freeze(users.filter((user) => normalizeUsername(user.username)).slice(0, 20)), candidates.replaceChildren(...candidateUsers.map((user, index) => {
	        const button = document.createElement("button");
	        button.type = "button", button.dataset.assignmentUserCandidate = String(index), button.setAttribute("role", "option"), button.setAttribute("aria-selected", String(
	          usernameKey(user.username) === usernameKey(verifiedUsername)
	        ));
	        const name = document.createElement("strong");
	        name.textContent = String(user.name ?? "").trim() || `@${user.username}`;
	        const detail = document.createElement("small");
	        return detail.textContent = `@${user.username}${user.id ? ` · #${user.id}` : ""}`, button.append(name, detail), button;
	      })), candidates.hidden = candidateUsers.length === 0, username.setAttribute(
	        "aria-expanded",
	        String(candidateUsers.length > 0)
	      );
	    }, scheduleLookup = (revealCandidates = !0) => {
	      if (cancelLookup(), !session.active) return;
	      verifiedInput = "", verifiedUsername = "", submit.disabled = !0, username.removeAttribute("aria-invalid"), clearCandidates();
	      const query = normalizeUsername(username.value);
	      if (!query) {
	        showLookupStatus(
	          "输入后会检索社区用户,只有真实用户名可以指定。",
	          "neutral"
	        );
	        return;
	      }
	      const sequence = lookupSequence;
	      showLookupStatus("正在检索用户…", "neutral"), lookupTimer = this.#scheduleLookup(() => {
	        lookupTimer = null, this.#users.searchUsers(query).then((users) => {
	          if (!session.active || sequence !== lookupSequence) return;
	          const key = usernameKey(query), numericId = /^\d+$/.test(query) ? Number(query) : null, visibleUsers = users.filter((user) => normalizeUsername(user.username)), matched = visibleUsers.find((user) => usernameKey(user.username) === key || Number.isSafeInteger(numericId) && numericId > 0 && user.id === numericId);
	          if (revealCandidates && renderCandidates(visibleUsers), matched) {
	            verifiedInput = key, verifiedUsername = normalizeUsername(matched.username), username.setAttribute("aria-invalid", "false"), submit.disabled = !1, showLookupStatus(
	              `已找到 @${verifiedUsername}。`,
	              "success"
	            );
	            return;
	          }
	          if (!visibleUsers.length) {
	            username.setAttribute("aria-invalid", "true"), showLookupStatus(
	              `没有找到“${query}”对应的社区用户。`,
	              "error"
	            );
	            return;
	          }
	          showLookupStatus(
	            `找到 ${visibleUsers.length} 个候选,请选择具体用户。`,
	            "neutral"
	          );
	        }).catch(() => {
	          !session.active || sequence !== lookupSequence || (clearCandidates(), username.setAttribute("aria-invalid", "true"), showLookupStatus("用户检索失败,请稍后重试。", "error"));
	        });
	      }, 240);
	    };
	    return username.addEventListener("input", () => scheduleLookup()), username.addEventListener("keydown", (event) => {
	      if (event.key === "Escape" && !candidates.hidden) {
	        event.preventDefault(), hideCandidates();
	        return;
	      }
	      if (event.key !== "ArrowDown" || candidates.hidden) return;
	      const first = candidates.querySelector("button");
	      first && (event.preventDefault(), first.focus({ preventScroll: !0 }));
	    }), candidates.addEventListener("click", (event) => {
	      const button = event.target?.closest(
	        "button[data-assignment-user-candidate]"
	      );
	      if (!button) return;
	      const user = candidateUsers[Number(button.dataset.assignmentUserCandidate)];
	      user && selectUser(user);
	    }), candidates.addEventListener("keydown", (event) => {
	      const buttons = [...candidates.querySelectorAll("button")], current = event.target?.closest(
	        "button[data-assignment-user-candidate]"
	      ), index = current ? buttons.indexOf(current) : -1;
	      if (event.key === "Escape") {
	        event.preventDefault(), hideCandidates(), username.focus({ preventScroll: !0 });
	        return;
	      }
	      if (!buttons.length || !["ArrowDown", "ArrowUp"].includes(event.key)) return;
	      event.preventDefault();
	      const offset = event.key === "ArrowDown" ? 1 : -1;
	      buttons[(index + offset + buttons.length) % buttons.length]?.focus({ preventScroll: !0 });
	    }), form.addEventListener("focusin", (event) => {
	      usernameLookup.contains(event.target) || hideCandidates();
	    }), form.addEventListener("submit", (event) => {
	      if (event.preventDefault(), session.busy) return;
	      const normalizedUsername = normalizeUsername(username.value), normalizedNote = note.value.trim();
	      if (!normalizedUsername) {
	        scheduleLookup(), username.focus();
	        return;
	      }
	      if (!verifiedUsername || verifiedInput !== usernameKey(normalizedUsername)) {
	        scheduleLookup(), username.focus();
	        return;
	      }
	      status.classList.remove("is-neutral"), session.resetStatus(), session.submit({
	        execute: () => request.submit(Object.freeze({
	          username: verifiedUsername,
	          note: normalizedNote
	        })),
	        setBusy: (busy) => (0, import_reader_action_form_support.setReaderActionFormBusy)(
	          form,
	          submit,
	          busy,
	          "指定中…",
	          "确认指定"
	        ),
	        successMessage: (message) => message || "指定已更新",
	        failureMessage: (cause) => cause instanceof Error ? cause.message : "指定失败,请重试"
	      });
	    }), session.mount(() => username.focus({ preventScroll: !0 })), scheduleLookup(!1), session.result;
	  }
	  destroy() {
	    this.#host.destroy();
	  }
	}
}, "bafbf9aad17fff35ba421f5cd5887e74f855572affd55569a16854e4972f8dff");

/* Source: lite/src/shell/reader-choice-form-surface.ts */
runtime.register("src/shell/reader-choice-form-surface.js", function(module, exports, require) {
	var reader_choice_form_surface_exports = {};
	__export(reader_choice_form_surface_exports, {
	  ReaderChoiceFormSurface: () => ReaderChoiceFormSurface
	});
	module.exports = __toCommonJS(reader_choice_form_surface_exports);
	var import_reader_action_form_support = require("./reader-action-form-support.js");
	class ReaderChoiceFormSurface {
	  scope;
	  #host;
	  constructor(options) {
	    this.#host = new import_reader_action_form_support.ReaderActionFormSurfaceHost({
	      ...options,
	      label: "ReaderChoiceFormSurface"
	    }), this.scope = this.#host.scope;
	  }
	  open(request) {
	    if (request.signal?.aborted) return Promise.resolve(!1);
	    const options = request.options.filter(
	      (option) => String(option.value).trim() && String(option.label).trim()
	    );
	    if (!options.length)
	      return Promise.reject(new Error("当前没有可用选项"));
	    const { id, previousFocus } = this.#host.prepare(), document = this.#host.document, titleId = `ldp-reader-choice-title-${id}`, submitLabel = request.submitLabel ?? "提交", frame = (0, import_reader_action_form_support.createReaderActionFormFrame)({
	      document,
	      titleId,
	      title: request.title,
	      intro: request.intro,
	      closeDataAttribute: "data-choice-close",
	      cancelDataAttribute: "data-choice-cancel",
	      submitLabel,
	      renderIcon: this.#host.renderIcon
	    }), { form, status, submit } = frame;
	    let firstChoice = null;
	    if (request.mode === "select") {
	      const field = document.createElement("label");
	      if (field.className = "ldp-reader-action-field", request.fieldLabel) {
	        const label = document.createElement("span");
	        label.textContent = request.fieldLabel, field.append(label);
	      }
	      const select = document.createElement("select");
	      select.className = "ldp-reader-select", select.name = `reader-choice-${id}`;
	      for (const option of options) {
	        const item = document.createElement("option");
	        item.value = option.value, item.textContent = option.label, item.selected = option.selected === !0, item.disabled = option.disabled === !0, select.append(item);
	      }
	      field.append(select), status.before(field), firstChoice = select;
	    } else {
	      const choices = document.createElement("div");
	      choices.className = "ldp-reader-action-options";
	      for (const option of options) {
	        const label = document.createElement("label");
	        label.className = "ldp-reader-action-option";
	        const input = document.createElement("input");
	        input.type = "checkbox", input.name = `reader-choice-${id}`, input.value = option.value, input.checked = option.selected === !0, input.disabled = option.disabled === !0;
	        const copy = document.createElement("span");
	        copy.className = "ldp-reader-action-option-copy";
	        const name = document.createElement("strong");
	        if (name.textContent = option.label, copy.append(name), option.description) {
	          const description = document.createElement("small");
	          description.textContent = option.description, copy.append(description);
	        }
	        label.append(input, copy), choices.append(label), !firstChoice && !input.disabled && (firstChoice = input);
	      }
	      status.before(choices);
	    }
	    const disabled = /* @__PURE__ */ new Map(), session = this.#host.start({
	      frame,
	      previousFocus,
	      closeSelector: "[data-choice-close]",
	      cancelSelector: "[data-choice-cancel]",
	      signal: request.signal
	    }), setBusy = (busy) => {
	      for (const control of form.querySelectorAll("input,select,button"))
	        busy && disabled.set(control, control.disabled), control.disabled = busy || disabled.get(control) === !0;
	      submit.textContent = busy ? request.busyLabel ?? "提交中…" : submitLabel;
	    };
	    return form.addEventListener("submit", (event) => {
	      if (event.preventDefault(), session.busy) return;
	      const values = [...form.querySelectorAll(
	        `[name="reader-choice-${id}"]`
	      )].flatMap((control) => {
	        if (control.tagName === "SELECT")
	          return control.value ? [control.value] : [];
	        const input = control;
	        return input.checked && !input.disabled ? [input.value] : [];
	      });
	      if (session.resetStatus(), !values.length) {
	        status.textContent = request.emptySelectionError ?? "请选择至少一个选项", firstChoice?.focus();
	        return;
	      }
	      session.submit({
	        execute: () => request.submit(Object.freeze(values)),
	        setBusy,
	        successMessage: (message) => message ?? request.successMessage ?? "操作已完成",
	        failureMessage: (cause) => cause instanceof Error ? cause.message : "操作失败,请重试"
	      });
	    }), session.mount(() => firstChoice?.focus({ preventScroll: !0 })), session.result;
	  }
	  destroy() {
	    this.#host.destroy();
	  }
	}
}, "723b9450b79cc805e438b79f39d532588d39337494ea98895e5324ea4bf12ebb");

/* Source: lite/src/shell/reader-embed-resize-controller.ts */
runtime.register("src/shell/reader-embed-resize-controller.js", function(module, exports, require) {
	var reader_embed_resize_controller_exports = {};
	__export(reader_embed_resize_controller_exports, {
	  ReaderEmbedResizeController: () => ReaderEmbedResizeController
	});
	module.exports = __toCommonJS(reader_embed_resize_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderEmbedResizeController {
	  scope;
	  #model;
	  #pageRoot;
	  #overlay;
	  #handle;
	  #readViewportWidth;
	  #onPersist;
	  #requestFrame;
	  #cancelFrame;
	  #pointer = null;
	  #frame = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#pageRoot = options.pageRoot, this.#overlay = options.overlay, this.#handle = options.handle, this.#readViewportWidth = options.readViewportWidth, this.#onPersist = options.onPersist, this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(this.#handle, "pointerdown", (event) => {
	      this.#onPointerDown(event);
	    }), this.scope.listen(this.#handle, "pointermove", (event) => {
	      this.#onPointerMove(event);
	    });
	    for (const type of ["pointerup", "pointercancel", "lostpointercapture"])
	      this.scope.listen(this.#handle, type, (event) => {
	        this.#finish(event, !0);
	      });
	    this.scope.listen(options.viewportTarget, "resize", () => {
	      this.#finish(void 0, !1), this.#model.resizeViewport(this.#readViewportWidth());
	    }), this.#model.changes.subscribe((snapshot) => {
	      snapshot.presentation.embedded || this.#finish(void 0, !1);
	    }, this.scope), this.scope.add(() => this.#finish(void 0, !1));
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #onPointerDown(event) {
	    if (!(this.#destroyed || event.button !== 0 || !this.#model.snapshot.presentation.embedded)) {
	      event.preventDefault(), event.stopPropagation(), this.#pointer = {
	        pointerId: event.pointerId,
	        startWidth: this.#model.snapshot.embedWidth,
	        clientX: event.clientX
	      }, this.#setResizing(!0);
	      try {
	        this.#handle.setPointerCapture(event.pointerId);
	      } catch {
	      }
	    }
	  }
	  #onPointerMove(event) {
	    !this.#pointer || event.pointerId !== this.#pointer.pointerId || (this.#pointer.clientX = event.clientX, this.#frame || (this.#frame = this.#requestFrame(() => this.#render())));
	  }
	  #render() {
	    this.#frame = 0;
	    const pointer = this.#pointer, presentation = this.#model.snapshot.presentation;
	    if (!pointer || !presentation.embedded) return;
	    const viewportWidth = this.#readViewportWidth(), requestedWidth = presentation.side === "left" ? pointer.clientX : viewportWidth - pointer.clientX;
	    this.#model.setEmbedWidth(requestedWidth);
	  }
	  #finish(event, persist = !1) {
	    const pointer = this.#pointer;
	    if (!pointer || event && event.pointerId !== pointer.pointerId) {
	      pointer || this.#setResizing(!1);
	      return;
	    }
	    this.#frame && (this.#cancelFrame(this.#frame), this.#frame = 0, persist && this.#render()), this.#pointer = null, persist || this.#model.setEmbedWidth(pointer.startWidth), this.#setResizing(!1);
	    try {
	      this.#handle.hasPointerCapture(pointer.pointerId) && this.#handle.releasePointerCapture(pointer.pointerId);
	    } catch {
	    }
	    persist && this.#model.snapshot.presentation.embedded && this.#onPersist?.(this.#model.snapshot.embedWidth);
	  }
	  #setResizing(active) {
	    this.#pageRoot.classList.toggle("ldp-reader-embed-resizing", active), this.#overlay.classList.toggle("ldp-reader-embed-resizing", active);
	  }
	}
}, "7667c373d4473cbe1d0054cde36715186d2fe45400e95cc5028541c45db701a2");

/* Source: lite/src/shell/reader-escape-surface.ts */
runtime.register("src/shell/reader-escape-surface.js", function(module, exports, require) {
	var reader_escape_surface_exports = {};
	__export(reader_escape_surface_exports, {
	  readerEscapeOwnedBy: () => readerEscapeOwnedBy,
	  readerFrontmostEscapeSurface: () => readerFrontmostEscapeSurface,
	  readerSurfaceQuery: () => readerSurfaceQuery,
	  readerSurfaceQueryAll: () => readerSurfaceQueryAll
	});
	module.exports = __toCommonJS(reader_escape_surface_exports);
	const ESCAPE_SURFACE_SELECTOR = [
	  ".ldp-reader-action-layer:not([hidden])",
	  ".ldp-ai-service-model-metadata:not([hidden])",
	  ".ldp-avatar-viewer",
	  ".ldp-native-boost-menu:not([hidden])",
	  '[data-identifier="ldp-native-boost-emoji-picker"]',
	  ".emoji-picker",
	  ".ldp-settings-popover:not([hidden])",
	  ".ldp-color-picker-popover:not([hidden])",
	  ".ldp-notifications-popover:not([hidden])",
	  ".ldp-history-popover:not([hidden])",
	  ".ldp-bookmarks-popover:not([hidden])",
	  ".ldp-lightbox",
	  ".ldp-lb-batch-overlay:not([hidden])",
	  ".ldp-descendant-replies-layer:not([hidden])",
	  ".ldp-code-preview-layer",
	  ".ldp-user-card-fallback.open",
	  ".ldp-user-card-follow-panel:not([hidden])",
	  ".ldp-user-card-follow-preview:not([hidden])",
	  ".ldp-reader-floating-window:not([hidden])",
	  ".ldp-reaction-picker:not([hidden])",
	  ".ldp-selection-toolbar:not([hidden])"
	].join(",");
	function visible(document, element) {
	  if (!element.isConnected || element.hidden || element.getAttribute("aria-hidden") === "true") return !1;
	  const viewport = document.defaultView;
	  if (!viewport?.getComputedStyle) return !0;
	  const style = viewport.getComputedStyle(element);
	  return style.display !== "none" && style.visibility !== "hidden";
	}
	function declaredSurfaceZIndex(element) {
	  for (const [selector, zIndex] of [
	    [".ldp-reader-action-layer", 2147483612],
	    [".ldp-ai-service-model-metadata", 2147483612],
	    [".ldp-user-card-follow-preview.is-above-user-observation-window", 2147483619],
	    [".ldp-user-card-follow-panel.is-above-user-observation-window", 2147483618],
	    [".ldp-user-card-fallback.is-above-user-observation-window", 2147483617],
	    [".ldp-user-card-follow-preview", 2147483611],
	    [".ldp-user-card-follow-panel", 2147483610],
	    [".ldp-color-picker-popover", 2147483610],
	    ['[data-identifier="ldp-native-boost-emoji-picker"]', 2147483610],
	    [".ldp-native-boost-emoji-picker", 2147483610],
	    [".ldp-selection-toolbar", 2147483610],
	    [".ldp-avatar-viewer", 2147483609],
	    [".ldp-native-boost-menu", 2147483608],
	    [".ldp-user-card-fallback", 2147483608],
	    [".ldp-reader-floating-window.is-user-observation-list", 2147483584],
	    [".ldp-settings-popover", 2147483606],
	    [".ldp-notifications-popover", 2147483606],
	    [".ldp-history-popover", 2147483606],
	    [".ldp-bookmarks-popover", 2147483606],
	    [".ldp-lightbox", 2147483600],
	    [".ldp-descendant-replies-layer-centered", 2147483590],
	    [".ldp-descendant-replies-layer", 30],
	    [".ldp-lb-batch-overlay", 12],
	    [".ldp-reaction-picker", 3]
	  ])
	    if (element.matches(selector)) return zIndex;
	  return null;
	}
	function stackingVector(document, element) {
	  const values = [], viewport = document.defaultView;
	  let current = element;
	  for (; current; ) {
	    const raw = viewport?.getComputedStyle ? viewport.getComputedStyle(current).zIndex : current.style.zIndex, normalized = String(raw ?? "").trim(), computed = normalized && normalized !== "auto" ? Number(normalized) : Number.NaN, value = Number.isFinite(computed) ? computed : declaredSurfaceZIndex(current);
	    value !== null && values.unshift(value), current = current.parentElement;
	  }
	  return values;
	}
	function compareVector(left, right) {
	  const length = Math.max(left.length, right.length);
	  for (let index = 0; index < length; index += 1) {
	    const difference = (left[index] ?? 0) - (right[index] ?? 0);
	    if (difference) return difference;
	  }
	  return left.length - right.length;
	}
	function escapeSurfaceRoots(document) {
	  const roots = [document];
	  for (const host of document.querySelectorAll(
	    "[data-ldp-reader-portal]"
	  ))
	    host.shadowRoot && roots.push(host.shadowRoot);
	  return roots;
	}
	function readerSurfaceQuery(document, selector) {
	  for (const root of escapeSurfaceRoots(document)) {
	    const surface = root.querySelector(selector);
	    if (surface) return surface;
	  }
	  return null;
	}
	function readerSurfaceQueryAll(document, selector) {
	  return Object.freeze(escapeSurfaceRoots(document).flatMap(
	    (root) => [...root.querySelectorAll(selector)]
	  ));
	}
	function readerFrontmostEscapeSurface(document) {
	  const candidates = escapeSurfaceRoots(document).flatMap(
	    (root) => [...root.querySelectorAll(ESCAPE_SURFACE_SELECTOR)]
	  ).filter((candidate) => visible(document, candidate));
	  let frontmost = null, frontmostVector = Object.freeze([]);
	  for (const candidate of candidates) {
	    const vector = stackingVector(document, candidate), comparison = compareVector(vector, frontmostVector), order = frontmost?.compareDocumentPosition(candidate) ?? 0;
	    (!frontmost || comparison > 0 || comparison === 0 && order & 4) && (frontmost = candidate, frontmostVector = vector);
	  }
	  return frontmost;
	}
	function readerEscapeOwnedBy(document, owners) {
	  const frontmost = readerFrontmostEscapeSurface(document);
	  return frontmost ? (Array.isArray(owners) ? owners : [owners]).some((owner) => owner === frontmost) : !0;
	}
}, "2c7a02acf9e893a104eef25e40506f26cad42004754ff606c5be4c7e3e345dfc");

/* Source: lite/src/shell/reader-exclusive-panel-coordinator.ts */
runtime.register("src/shell/reader-exclusive-panel-coordinator.js", function(module, exports, require) {
	var reader_exclusive_panel_coordinator_exports = {};
	__export(reader_exclusive_panel_coordinator_exports, {
	  ReaderExclusivePanelCoordinator: () => ReaderExclusivePanelCoordinator
	});
	module.exports = __toCommonJS(reader_exclusive_panel_coordinator_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderExclusivePanelCoordinator {
	  scope;
	  #entries;
	  #beforeOpen;
	  #onError;
	  #epoch = 0;
	  constructor(options) {
	    this.#entries = Object.freeze([...options.entries]), this.#beforeOpen = options.beforeOpen ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const triggers = /* @__PURE__ */ new Set();
	    for (const entry of this.#entries) {
	      if (!entry.id || triggers.has(entry.trigger))
	        throw new Error("Header 面板互斥项必须拥有唯一 id 与 trigger");
	      triggers.add(entry.trigger), this.scope.listen(entry.trigger, "click", (event) => {
	        event.preventDefault(), event.stopImmediatePropagation(), this.#activate(entry);
	      }, !0);
	    }
	    this.scope.add(() => {
	      this.#epoch += 1;
	      for (const entry of this.#entries)
	        entry.trigger.removeAttribute("aria-busy");
	    });
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #activate(target) {
	    const epoch = ++this.#epoch;
	    target.trigger.setAttribute("aria-busy", "true");
	    try {
	      if (target.isOpen() && !target.coexistGroup) {
	        await target.close();
	        return;
	      }
	      for (const entry of this.#entries) {
	        if (entry === target || !entry.isOpen() || target.coexistGroup && entry.coexistGroup === target.coexistGroup) continue;
	        if (await entry.close() === !1 || epoch !== this.#epoch || this.scope.destroyed)
	          return;
	      }
	      if (epoch !== this.#epoch || this.scope.destroyed || await this.#beforeOpen(target) === !1 || epoch !== this.#epoch || this.scope.destroyed)
	        return;
	      await target.open();
	    } catch (cause) {
	      !this.scope.destroyed && epoch === this.#epoch && this.#onError(cause);
	    } finally {
	      epoch === this.#epoch && target.trigger.removeAttribute("aria-busy");
	    }
	  }
	}
}, "ce50d509559082001f97cd7ab52fc9d5e6b60eb5098b0e656ff7f88b62f14b9d");

/* Source: lite/src/shell/reader-feedback-surface.ts */
runtime.register("src/shell/reader-feedback-surface.js", function(module, exports, require) {
	var reader_feedback_surface_exports = {};
	__export(reader_feedback_surface_exports, {
	  ReaderFeedbackSurface: () => ReaderFeedbackSurface
	});
	module.exports = __toCommonJS(reader_feedback_surface_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_event_target = require("../dom/event-target.js"), import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_reader_action_form_support = require("./reader-action-form-support.js"), import_reader_action_surface_coordinator = require("./reader-action-surface-coordinator.js");
	class ReaderFeedbackSurface {
	  scope;
	  #document;
	  #root;
	  #toastLifetimeMs;
	  #schedule;
	  #cancel;
	  #focusSoon;
	  #renderIcon;
	  #coordinator;
	  #confirmation = null;
	  #toast = null;
	  #toastTimer = null;
	  #id = 0;
	  constructor(options) {
	    if (this.#document = options.document, this.#root = options.root, this.#toastLifetimeMs = Number(options.toastLifetimeMs ?? 1800), !Number.isFinite(this.#toastLifetimeMs) || this.#toastLifetimeMs < 0)
	      throw new RangeError("toastLifetimeMs 必须是非负有限数值");
	    this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(
	      handle
	    )), this.#renderIcon = options.renderIcon, this.#focusSoon = options.focusSoon ?? ((callback) => {
	      const view = this.#document.defaultView;
	      view?.requestAnimationFrame ? view.requestAnimationFrame(callback) : queueMicrotask(callback);
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#coordinator = options.coordinator ?? new import_reader_action_surface_coordinator.ReaderActionSurfaceCoordinator({ parentScope: this.scope }), this.scope.add(() => {
	      this.#closeConfirmation("cancel"), this.#clearToast();
	    });
	  }
	  confirm(request) {
	    return this.choose(request).then((choice) => choice === "confirm");
	  }
	  choose(request, root = this.#root) {
	    this.#assertActive();
	    const previousFocus = this.#confirmation?.previousFocus ?? (0, import_event_target.deepActiveElement)(this.#document);
	    this.#closeConfirmation("cancel");
	    const id = ++this.#id, layer = this.#document.createElement("div");
	    layer.className = "ldp-reader-action-layer ldp-reader-confirm-layer";
	    const dialog = this.#document.createElement("section");
	    dialog.className = "ldp-reader-action-dialog ldp-reader-confirm-dialog" + (request.tone === "primary" ? " is-primary" : ""), dialog.setAttribute("role", "alertdialog"), dialog.setAttribute("aria-modal", "true");
	    const titleId = `ldp-reader-confirm-title-${id}`, descriptionId = `ldp-reader-confirm-description-${id}`, detailsId = `ldp-reader-confirm-details-${id}`, noteId = `ldp-reader-confirm-note-${id}`, details = (request.details ?? []).filter(
	      (detail) => String(detail.label).trim()
	    ), noteText = String(request.note ?? "").trim();
	    dialog.setAttribute("aria-labelledby", titleId), dialog.setAttribute(
	      "aria-describedby",
	      [
	        descriptionId,
	        details.length ? detailsId : "",
	        noteText ? noteId : ""
	      ].filter(Boolean).join(" ")
	    );
	    const content = this.#document.createElement("div");
	    content.className = "ldp-reader-confirm-content";
	    const icon = this.#document.createElement("span");
	    icon.className = "ldp-reader-confirm-icon", icon.setAttribute("aria-hidden", "true"), icon.append((0, import_reader_action_form_support.renderReaderActionIcon)(
	      this.#document,
	      request.icon ?? "alertTriangle",
	      this.#renderIcon
	    ));
	    const copy = this.#document.createElement("div");
	    copy.className = "ldp-reader-confirm-copy";
	    const title = this.#document.createElement("strong");
	    title.id = titleId, title.textContent = String(request.title || "确认操作");
	    const message = this.#document.createElement("p");
	    if (message.id = descriptionId, message.textContent = String(request.message ?? ""), copy.append(title, message), details.length) {
	      const list = this.#document.createElement("ul");
	      list.className = "ldp-reader-confirm-details", list.id = detailsId;
	      for (const detail of details) {
	        const item = this.#document.createElement("li"), label = this.#document.createElement("span");
	        label.textContent = detail.label;
	        const value = this.#document.createElement("strong");
	        value.textContent = detail.value ?? "", item.append(label, value), list.append(item);
	      }
	      copy.append(list);
	    }
	    if (noteText) {
	      const note = this.#document.createElement("small");
	      note.id = noteId, note.textContent = noteText, copy.append(note);
	    }
	    content.append(icon, copy);
	    const footer = this.#document.createElement("div");
	    footer.className = "ldp-reader-action-footer";
	    const cancel = this.#document.createElement("button");
	    cancel.type = "button", cancel.className = "ldp-reader-action-cancel", cancel.textContent = request.cancelLabel ?? "取消";
	    const secondary = request.secondaryLabel ? this.#document.createElement("button") : null;
	    secondary && (secondary.type = "button", secondary.className = "ldp-reader-action-secondary", secondary.textContent = request.secondaryLabel ?? "");
	    const submit = this.#document.createElement("button");
	    return submit.type = "button", submit.className = "ldp-reader-action-submit", submit.textContent = request.confirmLabel ?? "确认", footer.append(cancel), secondary && footer.append(secondary), footer.append(submit), dialog.append(content, footer), layer.append(dialog), new Promise((resolve) => {
	      let settled = !1, releaseAction = () => {
	      };
	      const settle = (value) => {
	        settled || (settled = !0, releaseAction(), layer.remove(), this.#confirmation?.layer === layer && (this.#confirmation = null), this.#focusSoon(() => {
	          previousFocus?.isConnected && typeof previousFocus.focus == "function" && previousFocus.focus({ preventScroll: !0 });
	        }), resolve(value));
	      };
	      this.#confirmation = { layer, previousFocus, settle }, releaseAction = this.#coordinator.claim(() => settle("cancel")), layer.addEventListener("click", (event) => {
	        const target = event.target;
	        target?.closest(".ldp-reader-action-secondary") ? settle("secondary") : event.target === layer || target?.closest(".ldp-reader-action-cancel") ? settle("cancel") : target?.closest(".ldp-reader-action-submit") && settle("confirm");
	      }), layer.addEventListener("keydown", (event) => {
	        const keyboard = event;
	        if (keyboard.key === "Escape") {
	          keyboard.preventDefault(), settle("cancel");
	          return;
	        }
	        if (keyboard.key !== "Tab") return;
	        const buttons = [cancel, secondary, submit].filter(
	          (button) => !!button
	        ).filter(
	          (button) => !button.disabled
	        );
	        if (!buttons.length) return;
	        const first = buttons[0], last = buttons.at(-1), active = (0, import_event_target.deepActiveElement)(this.#document);
	        keyboard.shiftKey && active === first ? (keyboard.preventDefault(), last.focus()) : !keyboard.shiftKey && active === last && (keyboard.preventDefault(), first.focus());
	      }), layer.addEventListener("wheel", (event) => (0, import_floating_surface_wheel.containFloatingSurfaceWheel)(layer, event), {
	        passive: !1
	      }), root.append(layer), this.#focusSoon(() => {
	        layer.isConnected && cancel.focus({ preventScroll: !0 });
	      });
	    });
	  }
	  show(message) {
	    this.#assertActive(), this.#clearToast();
	    const toast = this.#document.createElement("div");
	    toast.className = "ldp-selection-toast", toast.setAttribute("role", "status"), toast.setAttribute("aria-live", "polite"), toast.textContent = String(message), this.#root.append(toast), this.#toast = toast, this.#toastTimer = this.#schedule(() => {
	      this.#toast === toast && (this.#toast = null, this.#toastTimer = null), toast.remove();
	    }, this.#toastLifetimeMs);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #closeConfirmation(value) {
	    const active = this.#confirmation;
	    this.#confirmation = null, active?.settle(value);
	  }
	  #clearToast() {
	    this.#toastTimer !== null && (this.#cancel(this.#toastTimer), this.#toastTimer = null), this.#toast?.remove(), this.#toast = null;
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderFeedbackSurface 已销毁");
	  }
	}
}, "294a7bce2ea83ebcf2c371e4ce45567a795ad0cc232d4c66ecbd9c08d19e0279");

/* Source: lite/src/shell/reader-floating-window-frame.ts */
runtime.register("src/shell/reader-floating-window-frame.js", function(module, exports, require) {
	var reader_floating_window_frame_exports = {};
	__export(reader_floating_window_frame_exports, {
	  ReaderFloatingWindowFrame: () => ReaderFloatingWindowFrame,
	  restoreReaderFloatingWindowTabSession: () => restoreReaderFloatingWindowTabSession
	});
	module.exports = __toCommonJS(reader_floating_window_frame_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_escape_surface = require("./reader-escape-surface.js"), import_reader_workspace = require("./reader-workspace.js");
	const READER_FLOATING_WINDOW_LAUNCHERS = Object.freeze([
	  Object.freeze({ selector: ".ldp-notifications-toggle", id: "notifications" }),
	  Object.freeze({ selector: ".ldp-history-toggle", id: "history" }),
	  Object.freeze({ selector: ".ldp-bookmarks-toggle", id: "bookmarks" }),
	  Object.freeze({
	    selector: ".ldp-topic-action-rail-download",
	    id: "topic-downloads"
	  }),
	  Object.freeze({
	    selector: ".ldp-topic-action-rail-user-observation",
	    id: "user-observations"
	  }),
	  Object.freeze({
	    selector: ".ldp-topic-action-rail-chronicle",
	    id: "chronicle"
	  }),
	  Object.freeze({
	    selector: ".ldp-topic-action-rail-unwanted-topics",
	    id: "unwanted-topics"
	  })
	]), READER_FLOATING_WINDOW_LAUNCHER_SELECTOR = READER_FLOATING_WINDOW_LAUNCHERS.map(({ selector }) => selector).join(","), READER_FLOATING_WINDOW_SCROLLBAR_GUARD_PX = 6, floatingWindowTabGroups = /* @__PURE__ */ new WeakMap();
	function eventPathMatches(event, selector) {
	  const composed = typeof event.composedPath == "function" ? event.composedPath() : [event.target];
	  return (composed.length ? composed : [event.target]).some((target) => {
	    const matches = target?.matches;
	    return typeof matches == "function" && matches.call(target, selector);
	  });
	}
	function pointerHitsHorizontalScrollbar(event, element) {
	  if (element.scrollWidth <= element.clientWidth + 1) return !1;
	  const bounds = element.getBoundingClientRect();
	  if (bounds.width <= 0 || bounds.height <= 0) return !1;
	  const measuredHeight = Math.max(
	    0,
	    Number(element.offsetHeight) - Number(element.clientHeight)
	  ), guardHeight = Math.max(
	    READER_FLOATING_WINDOW_SCROLLBAR_GUARD_PX,
	    measuredHeight
	  );
	  return event.clientX >= bounds.left && event.clientX <= bounds.right && event.clientY >= bounds.bottom - guardHeight && event.clientY <= bounds.bottom;
	}
	class ReaderFloatingWindowTabGroup {
	  #document;
	  #mount;
	  #frames = /* @__PURE__ */ new Map();
	  #opened = [];
	  #claimedOutsideEvents = /* @__PURE__ */ new WeakSet();
	  #onPointerDown;
	  #onClick;
	  #active = null;
	  #sharedGeometry = null;
	  #pinned = !1;
	  #tabScrollLeft = 0;
	  #visible = !1;
	  constructor(document, mount) {
	    this.#document = document, this.#mount = mount, this.#onPointerDown = (event) => {
	      eventPathMatches(event, ".ldp-reader-floating-window-add-wrap") || this.#closeMenus();
	    }, this.#onClick = (event) => {
	      const launcher = READER_FLOATING_WINDOW_LAUNCHERS.find(({ selector }) => eventPathMatches(event, selector)), frame = launcher ? this.#frames.get(launcher.id) : null;
	      frame?.isOpen && (event.preventDefault(), event.stopImmediatePropagation(), frame.open());
	    }, this.#document.addEventListener("pointerdown", this.#onPointerDown, !0), this.#document.addEventListener("click", this.#onClick, !0);
	  }
	  register(frame) {
	    const existing = this.#frames.get(frame.tabId);
	    if (existing && existing !== frame)
	      throw new Error(`浮窗标签 id 重复:${frame.tabId}`);
	    this.#captureTabScroll(), this.#captureSharedGeometry(), this.#frames.size ? frame.syncSharedPinned(this.#pinned) : this.#pinned = frame.pinned, this.#frames.set(frame.tabId, frame), this.#sync();
	  }
	  unregister(frame) {
	    if (this.#frames.get(frame.tabId) !== frame) return;
	    this.#captureTabScroll(), this.#captureSharedGeometry(), this.#frames.delete(frame.tabId);
	    const index = this.#opened.indexOf(frame);
	    index >= 0 && this.#opened.splice(index, 1), this.#active === frame && (this.#active = this.#opened[Math.min(index, this.#opened.length - 1)] ?? this.#opened.at(-1) ?? null), this.#opened.length || (this.#visible = !1), this.#sync(), !this.#frames.size && (this.#document.removeEventListener(
	      "pointerdown",
	      this.#onPointerDown,
	      !0
	    ), this.#document.removeEventListener("click", this.#onClick, !0), floatingWindowTabGroups.get(this.#mount) === this && floatingWindowTabGroups.delete(this.#mount));
	  }
	  open(frame) {
	    if (!this.#frames.has(frame.tabId)) return;
	    this.#captureTabScroll(), this.#visible ? this.#captureSharedGeometry() : this.#sharedGeometry = frame.geometry.snapshot.geometry;
	    const revealActive = !this.#opened.includes(frame);
	    revealActive && this.#opened.push(frame), this.#active = frame, this.#visible = !0, this.#sync(revealActive);
	  }
	  close(frame) {
	    const index = this.#opened.indexOf(frame);
	    index < 0 || (this.#captureTabScroll(), this.#captureSharedGeometry(), this.#opened.splice(index, 1), this.#active === frame && (this.#active = this.#opened[Math.min(index, this.#opened.length - 1)] ?? this.#opened.at(-1) ?? null), this.#opened.length || (this.#visible = !1), this.#sync());
	  }
	  activate(frame) {
	    !frame.isOpen || !this.#opened.includes(frame) || frame.open();
	  }
	  dismissFromPointerEvent(frame, event) {
	    return this.#active !== frame || this.#claimedOutsideEvents.has(event) ? !1 : (this.#claimedOutsideEvents.add(event), this.#dismiss(), !0);
	  }
	  dismissFromEscapeEvent(frame, event) {
	    return event.key !== "Escape" || this.#active !== frame || !(0, import_reader_escape_surface.readerEscapeOwnedBy)(this.#document, [frame.element]) ? !1 : (event.preventDefault(), event.stopImmediatePropagation(), this.#pinned ? this.#closeMenus() : this.#dismiss(), !0);
	  }
	  isLauncherEvent(event) {
	    return eventPathMatches(event, READER_FLOATING_WINDOW_LAUNCHER_SELECTOR);
	  }
	  get pinned() {
	    return this.#pinned;
	  }
	  syncPinnedFrom(frame) {
	    if (this.#frames.get(frame.tabId) === frame) {
	      this.#pinned = frame.pinned;
	      for (const target of this.#frames.values())
	        target.syncSharedPinned(this.#pinned);
	    }
	  }
	  refresh() {
	    this.#captureTabScroll(), this.#captureSharedGeometry(), this.#sync();
	  }
	  restore(tabId) {
	    if (this.#visible || !this.#opened.length) return !1;
	    if (this.#captureSharedGeometry(), tabId) {
	      const requested = this.#frames.get(tabId);
	      if (!requested || !this.#opened.includes(requested)) return !1;
	      this.#active = requested;
	    } else (!this.#active || !this.#opened.includes(this.#active)) && (this.#active = this.#opened.at(-1) ?? null);
	    return this.#visible = !!this.#active, this.#sync(), this.#visible;
	  }
	  #closeMenus() {
	    for (const frame of this.#frames.values()) frame.closeAddMenu();
	  }
	  #dismiss() {
	    this.#captureTabScroll(), this.#captureSharedGeometry(), this.#visible = !1, this.#sync();
	  }
	  #captureSharedGeometry() {
	    this.#active && (this.#sharedGeometry = this.#active.geometry.snapshot.geometry);
	  }
	  #captureTabScroll() {
	    if (!this.#active?.active) return;
	    const scrollLeft = Number(this.#active.tabList.scrollLeft);
	    Number.isFinite(scrollLeft) && (this.#tabScrollLeft = Math.max(0, scrollLeft));
	  }
	  #sync(revealActive = !1) {
	    !this.#sharedGeometry && this.#active && (this.#sharedGeometry = this.#active.geometry.snapshot.geometry), this.#active && this.#sharedGeometry && this.#active.applySharedGeometry(this.#sharedGeometry);
	    const remaining = [...this.#frames.values()].filter((frame) => !this.#opened.includes(frame)).sort((left, right) => left.tabOrder - right.tabOrder);
	    for (const frame of this.#frames.values())
	      frame.syncTabVisibility(this.#visible && frame === this.#active);
	    this.#active?.renderTabChrome(
	      this.#opened,
	      remaining,
	      this.#tabScrollLeft,
	      revealActive
	    ), this.#captureTabScroll();
	  }
	}
	function tabGroup(document, mount) {
	  const existing = floatingWindowTabGroups.get(mount);
	  if (existing) return existing;
	  const created = new ReaderFloatingWindowTabGroup(document, mount);
	  return floatingWindowTabGroups.set(mount, created), created;
	}
	function restoreReaderFloatingWindowTabSession(mount, tabId) {
	  return floatingWindowTabGroups.get(mount)?.restore(tabId) ?? !1;
	}
	function storedPreferences(storage, key) {
	  try {
	    const raw = storage?.getItem(key);
	    if (!raw) return null;
	    const source = JSON.parse(raw), width = Number(source.readerWindowWidth), height = Number(source.readerWindowHeight), left = Number(source.readerWindowX), top = Number(source.readerWindowY);
	    return !Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0 || !Number.isFinite(left) || !Number.isFinite(top) ? null : Object.freeze({
	      readerWindowWidth: width,
	      readerWindowHeight: height,
	      readerWindowX: left,
	      readerWindowY: top,
	      readerWindowLocked: !1,
	      readerWindowPinned: source.readerWindowPinned === !0
	    });
	  } catch {
	    return null;
	  }
	}
	function viewport(document, mount) {
	  const view = document.defaultView;
	  return Object.freeze({
	    width: Math.max(
	      1,
	      Number(view?.innerWidth) || document.documentElement?.clientWidth || mount.clientWidth || 1024
	    ),
	    height: Math.max(
	      1,
	      Number(view?.innerHeight) || document.documentElement?.clientHeight || mount.clientHeight || 768
	    )
	  });
	}
	function defaultPreferences(options, width, height) {
	  const margin = Number(options.policy.margin ?? 8), targetWidth = Math.min(
	    Math.max(1, width - margin * 2),
	    options.policy.defaultWidth
	  ), targetHeight = Math.min(
	    Math.max(1, height - margin * 2),
	    options.policy.defaultHeight
	  ), placement = options.placement ?? "center", left = placement === "right" ? width - targetWidth - Math.max(18, margin) : placement === "left" ? Math.max(18, margin) : Math.round((width - targetWidth) / 2);
	  return Object.freeze({
	    readerWindowWidth: targetWidth,
	    readerWindowHeight: targetHeight,
	    readerWindowX: left,
	    readerWindowY: placement === "center" ? Math.round((height - targetHeight) / 2) : Math.max(32, margin),
	    readerWindowLocked: !1,
	    readerWindowPinned: !1
	  });
	}
	class ReaderFloatingWindowFrame {
	  scope;
	  host;
	  element;
	  header;
	  title;
	  meta;
	  body;
	  actions;
	  tabRow;
	  toolbarRow;
	  tabList;
	  addWrap;
	  addButton;
	  addMenu;
	  pinButton;
	  closeButton;
	  tabId;
	  tabOrder;
	  geometry;
	  pointer;
	  #onClose;
	  #baseZIndex;
	  #geometryStorage;
	  #geometryStorageKey;
	  #requestOpen;
	  #tabAction;
	  #notify;
	  #tabGroup;
	  #launcherSelector;
	  #tabLabel;
	  #iconName;
	  #open = !1;
	  #active = !1;
	  constructor(options) {
	    this.#onClose = options.onClose ?? (() => {
	    }), this.#baseZIndex = options.zIndex, this.#geometryStorage = options.geometryStorage, this.#geometryStorageKey = options.geometryStorageKey, this.#requestOpen = options.requestOpen, this.#tabAction = options.tabAction ?? null, this.#tabAction?.classList.add("ldp-reader-floating-window-tab-action");
	    const standalone = options.sessionMode === "standalone";
	    this.#launcherSelector = standalone ? options.launcherSelector ?? null : null, this.#notify = options.notify ?? (() => {
	    }), this.tabId = options.tabId, this.tabOrder = options.tabOrder, this.#tabLabel = options.title, this.#iconName = options.icon, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.host = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      `ldp-reader-floating-host is-${options.variant}`
	    ), this.host.style.zIndex = String(options.zIndex), this.host.classList.toggle("is-standalone", standalone), options.mount.append(this.host), this.element = (0, import_html_element.htmlElement)(
	      options.document,
	      "section",
	      `ldp-reader-floating-window is-${options.variant}`
	    ), this.element.hidden = !0, this.element.classList.toggle("is-standalone", standalone), this.element.setAttribute("role", "dialog"), this.element.setAttribute("aria-label", options.ariaLabel), this.header = (0, import_html_element.htmlElement)(
	      options.document,
	      "header",
	      "ldp-reader-floating-window-head"
	    ), this.header.dataset.readerFloatingDragSurface = options.variant, this.title = (0, import_html_element.htmlElement)(
	      options.document,
	      "strong",
	      "ldp-reader-floating-window-title",
	      options.title
	    ), this.title.hidden = !standalone, this.tabList = (0, import_html_element.htmlElement)(
	      options.document,
	      "nav",
	      "ldp-reader-floating-window-tabs"
	    ), this.tabList.setAttribute("role", "tablist"), this.tabList.setAttribute("aria-label", "已打开的工具浮窗"), this.addWrap = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-add-wrap"
	    ), this.addButton = options.document.createElement("button"), this.addButton.type = "button", this.addButton.className = "ldp-reader-floating-window-add", this.addButton.setAttribute("aria-label", "添加工具浮窗"), this.addButton.setAttribute("aria-haspopup", "menu"), this.addButton.setAttribute("aria-expanded", "false"), this.addButton.append((0, import_reader_icon.createReaderIcon)(options.document, "plus")), this.addMenu = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-add-menu"
	    ), this.addMenu.hidden = !0, this.addMenu.setAttribute("role", "menu"), this.addMenu.setAttribute("aria-label", "添加剩余工具浮窗"), this.addWrap.append(this.addButton, this.addMenu), this.meta = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-reader-floating-window-meta"
	    ), this.pinButton = options.document.createElement("button"), this.pinButton.type = "button", this.pinButton.className = "ldp-reader-floating-window-pin", this.pinButton.setAttribute("aria-label", "锁定置顶,点击外部保持显示"), this.pinButton.setAttribute("aria-pressed", "false"), this.pinButton.append((0, import_reader_icon.createReaderIcon)(options.document, "pin")), this.closeButton = options.document.createElement("button"), this.closeButton.type = "button", this.closeButton.className = "ldp-reader-floating-window-close", this.closeButton.setAttribute("aria-label", `关闭${options.title}`), this.closeButton.append((0, import_reader_icon.createReaderIcon)(options.document, "x")), this.actions = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-actions"
	    ), this.tabRow = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-tab-row"
	    ), this.tabRow.append(this.tabList, this.addWrap, this.pinButton), this.toolbarRow = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-toolbar-row"
	    ), this.toolbarRow.append(this.meta, this.actions), standalone ? this.header.append(
	      this.title,
	      this.meta,
	      this.pinButton,
	      ...this.#tabAction ? [this.#tabAction] : [],
	      this.closeButton
	    ) : this.header.append(
	      this.title,
	      this.tabRow,
	      this.toolbarRow
	    ), this.body = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-reader-floating-window-body"
	    );
	    const handles = Object.freeze([
	      "n",
	      "s",
	      "e",
	      "w",
	      "ne",
	      "nw",
	      "se",
	      "sw"
	    ]).map((direction) => {
	      const handle = (0, import_html_element.htmlElement)(
	        options.document,
	        "span",
	        "ldp-reader-floating-window-resize"
	      );
	      return handle.dataset.readerResize = direction, handle.dataset.resize = direction, handle.setAttribute("aria-hidden", "true"), handle;
	    });
	    this.element.append(this.header, this.body, ...handles), this.host.append(this.element), this.scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(this.element));
	    const currentViewport = viewport(options.document, options.mount), restored = storedPreferences(
	      options.geometryStorage,
	      options.geometryStorageKey
	    );
	    this.geometry = new import_reader_workspace.ReaderWindowGeometryModel({
	      preferences: restored ?? defaultPreferences(
	        options,
	        currentViewport.width,
	        currentViewport.height
	      ),
	      viewportWidth: currentViewport.width,
	      viewportHeight: currentViewport.height,
	      mode: "floating",
	      policy: {
	        margin: 8,
	        minWidth: 420,
	        minHeight: 360,
	        compactWidth: 0,
	        defaultViewportWidth: 0.8,
	        defaultViewportHeight: 0.82,
	        ...options.policy
	      }
	    }), this.geometry.changes.subscribe(
	      (snapshot) => this.#applyGeometry(snapshot),
	      this.scope
	    );
	    const view = options.document.defaultView, requestFrame = (callback) => typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(callback) : (callback(0), 0), cancelFrame = (id) => {
	      view?.cancelAnimationFrame?.(id);
	    };
	    this.pointer = new import_reader_workspace.ReaderWindowPointerController({
	      model: this.geometry,
	      overlay: this.element,
	      modal: this.element,
	      header: this.header,
	      pinButton: this.pinButton,
	      ...view ? { viewportTarget: view } : {},
	      readViewport: () => viewport(options.document, options.mount),
	      onPersist: (preferences) => {
	        try {
	          options.geometryStorage?.setItem(
	            options.geometryStorageKey,
	            JSON.stringify(preferences)
	          );
	        } catch {
	          options.notify?.(`${options.title}浮窗位置保存失败`);
	        }
	      },
	      requestFrame,
	      cancelFrame,
	      dragSurfaceSelector: ".ldp-reader-floating-window-head[data-reader-floating-drag-surface]",
	      blockedSelector: 'button,input,select,textarea,label,a,[role="button"],[contenteditable="true"]',
	      isDragBlocked: (event, target) => {
	        const tabs = target.closest(
	          ".ldp-reader-floating-window-tabs"
	        );
	        return tabs === this.tabList && pointerHitsHorizontalScrollbar(event, tabs);
	      },
	      interactingClassName: "ldp-reader-floating-window-interacting",
	      restingTransform: "none",
	      projectPlacement: () => {
	      },
	      parentScope: this.scope
	    }), this.#applyGeometry(this.geometry.snapshot), this.scope.listen(this.closeButton, "click", () => this.close()), this.scope.listen(this.addButton, "click", (event) => {
	      if (event.preventDefault(), event.stopPropagation(), this.addButton.disabled) return;
	      const open = this.addButton.getAttribute("aria-expanded") === "true";
	      this.addButton.setAttribute("aria-expanded", String(!open)), this.addMenu.hidden = open;
	    }), this.scope.listen(this.tabList, "wheel", (eventValue) => {
	      const event = eventValue;
	      if (this.tabList.scrollWidth <= this.tabList.clientWidth + 1) return;
	      const delta = Math.abs(event.deltaX) >= Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
	      delta && (this.tabList.scrollLeft += delta, event.preventDefault(), event.stopPropagation());
	    }, { passive: !1 }), view && this.scope.listen(view, "resize", () => {
	      const next = viewport(options.document, options.mount);
	      this.geometry.resizeViewport(next.width, next.height);
	    }), this.#tabGroup = standalone ? null : tabGroup(options.document, options.mount), this.#tabGroup?.register(this), this.#tabGroup && this.scope.listen(this.pinButton, "click", () => {
	      this.#tabGroup?.syncPinnedFrom(this);
	    }), this.scope.add(() => {
	      this.#tabGroup?.unregister(this), this.host.remove();
	    });
	  }
	  get isOpen() {
	    return this.#open;
	  }
	  get pinned() {
	    return this.geometry.snapshot.pinned;
	  }
	  get active() {
	    return this.#active;
	  }
	  setIcon(name) {
	    this.#iconName = name, this.#tabGroup?.refresh();
	  }
	  setTitle(value) {
	    this.title.textContent = value, this.#tabGroup?.refresh();
	  }
	  setMinimumWidth(width) {
	    this.geometry.setMinimumWidth(width);
	  }
	  applySharedGeometry(geometry) {
	    this.geometry.setGeometry(
	      geometry.width,
	      geometry.height,
	      geometry.left,
	      geometry.top
	    );
	  }
	  open() {
	    this.scope.destroyed || (this.#syncSharedGeometry(), this.#open = !0, this.element.classList.add("is-open"), this.#tabGroup ? this.#tabGroup.open(this) : this.syncTabVisibility(!0));
	  }
	  #syncSharedGeometry() {
	    const restored = storedPreferences(
	      this.#geometryStorage,
	      this.#geometryStorageKey
	    );
	    restored && this.geometry.setGeometry(
	      restored.readerWindowWidth,
	      restored.readerWindowHeight,
	      restored.readerWindowX,
	      restored.readerWindowY
	    ), this.#tabGroup && this.syncSharedPinned(this.#tabGroup.pinned);
	  }
	  close() {
	    !this.#open || this.scope.destroyed || (this.#open = !1, this.element.classList.remove("is-open"), this.#tabGroup ? this.#tabGroup.close(this) : this.syncTabVisibility(!1), this.#onClose());
	  }
	  dismissFromPointerEvent(event) {
	    return !this.#open || !this.#active || this.pinned || this.contains(event.target) || eventPathMatches(event, ".ldp-reader-floating-window") || this.#launcherSelector && eventPathMatches(event, this.#launcherSelector) || this.#tabGroup?.isLauncherEvent(event) ? !1 : this.#tabGroup ? this.#tabGroup.dismissFromPointerEvent(this, event) : (this.close(), !0);
	  }
	  dismissFromEscapeEvent(event) {
	    return !this.#open || !this.#active ? !1 : this.#tabGroup ? this.#tabGroup.dismissFromEscapeEvent(this, event) : event.key !== "Escape" || !(0, import_reader_escape_surface.readerEscapeOwnedBy)(this.#document(), [this.element]) ? !1 : (event.preventDefault(), event.stopImmediatePropagation(), this.pinned || this.close(), !0);
	  }
	  syncSharedPinned(pinned) {
	    this.pinned !== pinned && this.geometry.setPinned(pinned);
	  }
	  syncTabVisibility(active) {
	    this.#active = active && this.#open, this.element.hidden = !this.#active, this.element.classList.toggle("is-active-tab", this.#active), this.#active || this.closeAddMenu();
	  }
	  renderTabChrome(opened, remaining, scrollLeft, revealActive) {
	    !this.#active || !this.#tabGroup || (this.tabList.replaceChildren(...opened.map((frame) => {
	      const item = (0, import_html_element.htmlElement)(
	        this.#document(),
	        "div",
	        "ldp-reader-floating-window-tab"
	      );
	      item.dataset.floatingTab = frame.tabId, item.setAttribute("role", "presentation");
	      const activate = this.#document().createElement("button");
	      activate.type = "button", activate.className = "ldp-reader-floating-window-tab-activate", activate.setAttribute("role", "tab"), activate.setAttribute("aria-selected", String(frame === this)), activate.setAttribute(
	        "aria-label",
	        `切换到${frame.#tabLabel}`
	      ), activate.append(
	        (0, import_reader_icon.createReaderIcon)(this.#document(), frame.#iconName),
	        (0, import_html_element.htmlElement)(
	          this.#document(),
	          "span",
	          "ldp-reader-floating-window-tab-title",
	          frame.#tabLabel
	        )
	      ), activate.addEventListener("click", () => {
	        this.#tabGroup?.activate(frame);
	      });
	      const close = frame === this ? this.closeButton : this.#document().createElement("button");
	      return frame !== this && (close.type = "button", close.className = "ldp-reader-floating-window-close", close.append((0, import_reader_icon.createReaderIcon)(this.#document(), "x")), close.addEventListener("click", () => frame.close())), close.dataset.floatingTabClose = frame.tabId, close.setAttribute(
	        "aria-label",
	        `关闭${frame.#tabLabel}`
	      ), item.classList.toggle("is-active", frame === this), item.addEventListener("pointerdown", (event) => {
	        event.button === 1 && (event.preventDefault(), event.stopPropagation());
	      }), item.addEventListener("auxclick", (event) => {
	        event.button === 1 && (event.preventDefault(), event.stopPropagation(), frame.close());
	      }), item.append(
	        activate,
	        ...frame === this && frame.#tabAction ? [frame.#tabAction] : [],
	        close
	      ), item;
	    })), this.tabList.scrollLeft = Math.max(0, scrollLeft), revealActive && this.#revealActiveTab(), this.addButton.disabled = remaining.length === 0, this.addButton.setAttribute(
	      "aria-label",
	      remaining.length ? `添加工具浮窗,剩余 ${remaining.length} 个` : "所有工具浮窗均已打开"
	    ), this.addMenu.replaceChildren(...remaining.map((frame) => {
	      const button = this.#document().createElement("button");
	      return button.type = "button", button.className = "ldp-reader-floating-window-add-option", button.dataset.floatingTabAdd = frame.tabId, button.setAttribute("role", "menuitem"), button.append(
	        (0, import_reader_icon.createReaderIcon)(this.#document(), frame.#iconName),
	        this.#document().createTextNode(frame.#tabLabel)
	      ), button.addEventListener("click", () => {
	        this.closeAddMenu(), frame.requestOpenFromTabs();
	      }), button;
	    })), remaining.length || this.closeAddMenu());
	  }
	  #revealActiveTab() {
	    const activeTab = this.tabList.querySelector(
	      ".ldp-reader-floating-window-tab.is-active"
	    );
	    if (!activeTab || this.tabList.clientWidth <= 0) return;
	    const viewportStart = this.tabList.scrollLeft, viewportEnd = viewportStart + this.tabList.clientWidth, tabStart = activeTab.offsetLeft, tabEnd = tabStart + activeTab.offsetWidth;
	    tabStart < viewportStart ? this.tabList.scrollLeft = tabStart : tabEnd > viewportEnd && (this.tabList.scrollLeft = tabEnd - this.tabList.clientWidth);
	  }
	  closeAddMenu() {
	    this.addButton.setAttribute("aria-expanded", "false"), this.addMenu.hidden = !0;
	  }
	  async requestOpenFromTabs() {
	    try {
	      await this.#requestOpen();
	    } catch (cause) {
	      this.#notify(
	        `${this.title.textContent ?? "工具"}浮窗打开失败:${String(cause)}`
	      );
	    }
	  }
	  contains(target) {
	    return !!(target && typeof target.nodeType == "number" && this.element.contains(target));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #applyGeometry(snapshot) {
	    if (!snapshot.managed) return;
	    this.host.classList.toggle("is-pinned", snapshot.pinned), this.element.classList.toggle("is-pinned", snapshot.pinned), this.host.style.zIndex = String(snapshot.pinned ? Math.min(2147483647, this.#baseZIndex + 32) : this.#baseZIndex), this.pinButton.classList.toggle("is-active", snapshot.pinned), this.pinButton.setAttribute("aria-pressed", String(snapshot.pinned));
	    const pinLabel = snapshot.pinned ? "取消锁定置顶" : "锁定置顶,点击外部保持显示";
	    this.pinButton.setAttribute("aria-label", pinLabel), this.pinButton.title = pinLabel;
	    const geometry = snapshot.geometry;
	    this.element.style.left = `${geometry.left}px`, this.element.style.top = `${geometry.top}px`, this.element.style.width = `${geometry.width}px`, this.element.style.height = `${geometry.height}px`;
	  }
	  #document() {
	    return this.element.ownerDocument;
	  }
	}
}, "8fc4ee5517b90edffc06aa4d0976cdc95f163ef634756c47bd3833e077db482d");

/* Source: lite/src/shell/reader-rate-limit-notice.ts */
runtime.register("src/shell/reader-rate-limit-notice.js", function(module, exports, require) {
	var reader_rate_limit_notice_exports = {};
	__export(reader_rate_limit_notice_exports, {
	  ReaderRateLimitNotice: () => ReaderRateLimitNotice
	});
	module.exports = __toCommonJS(reader_rate_limit_notice_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderRateLimitNotice {
	  scope;
	  #document;
	  #elements;
	  #snapshot;
	  #intervalMs;
	  #timer = null;
	  #epoch = 0;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#document = options.document, this.#elements = options.elements, this.#snapshot = options.snapshot, this.#intervalMs = Math.max(250, options.intervalMs ?? 1e3), options.challengeHref ? (this.#elements.challenge.href = options.challengeHref, this.#elements.challenge.hidden = !1) : (this.#elements.challenge.removeAttribute("href"), this.#elements.challenge.hidden = !0), this.scope.listen(this.#document, "visibilitychange", () => {
	      this.#syncPolling();
	    }), this.scope.add(() => {
	      this.#epoch += 1, this.#stopPolling(), this.#hide();
	    }), this.#syncPolling();
	  }
	  async refresh() {
	    if (this.scope.destroyed) return;
	    const epoch = ++this.#epoch;
	    try {
	      const snapshot = await this.#snapshot();
	      if (this.scope.destroyed || epoch !== this.#epoch) return;
	      if (snapshot.challengeState === "required") {
	        this.#elements.detail.textContent = "关键 Reader 请求遇到 Cloudflare 验证,已暂停后续请求;点击右侧按钮只打开一个人工验证浮窗。", this.#show();
	        return;
	      }
	      if (snapshot.challengeState === "active") {
	        this.#elements.detail.textContent = snapshot.challengeOwned ? "本页过盾浮窗已打开;若未显示,点击右侧按钮唤起。验证完成后请求自动恢复。" : "其他标签页已有唯一过盾浮窗;点击右侧按钮可唤起。验证完成后请求自动恢复。", this.#show();
	        return;
	      }
	      this.#hide();
	    } catch {
	      !this.scope.destroyed && epoch === this.#epoch && this.#hide();
	    }
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #show() {
	    delete this.#elements.root.dataset.cooldownSeconds, this.#elements.root.hidden = !1;
	  }
	  #hide() {
	    this.#elements.root.hidden = !0, delete this.#elements.root.dataset.cooldownSeconds;
	  }
	  #syncPolling() {
	    this.#stopPolling(), !(this.scope.destroyed || this.#document.visibilityState === "hidden") && (this.refresh(), this.#timer = setInterval(() => {
	      this.refresh();
	    }, this.#intervalMs));
	  }
	  #stopPolling() {
	    this.#timer !== null && (clearInterval(this.#timer), this.#timer = null);
	  }
	}
}, "105e5418a41d0b484798de92b32252448685ee89a0148d676fd36f049f53a13b");

/* Source: lite/src/shell/reader-report-form-surface.ts */
runtime.register("src/shell/reader-report-form-surface.js", function(module, exports, require) {
	var reader_report_form_surface_exports = {};
	__export(reader_report_form_surface_exports, {
	  ReaderReportFormSurface: () => ReaderReportFormSurface
	});
	module.exports = __toCommonJS(reader_report_form_surface_exports);
	var import_reader_action_form_support = require("./reader-action-form-support.js");
	class ReaderReportFormSurface {
	  scope;
	  #host;
	  constructor(options) {
	    this.#host = new import_reader_action_form_support.ReaderActionFormSurfaceHost({
	      ...options,
	      label: "ReaderReportFormSurface"
	    }), this.scope = this.#host.scope;
	  }
	  open(request) {
	    const options = request.options.filter(
	      (option) => Number.isSafeInteger(option.id) && option.id > 0 && String(option.label).trim()
	    );
	    if (!options.length)
	      return Promise.reject(new Error("当前没有可用的举报类型"));
	    const messageMaxLength = Number(request.messageMaxLength);
	    if (!Number.isSafeInteger(messageMaxLength) || messageMaxLength <= 0)
	      return Promise.reject(new Error("举报说明长度限制无效"));
	    const { id, previousFocus } = this.#host.prepare(), document = this.#host.document, titleId = `ldp-reader-report-title-${id}`, frame = (0, import_reader_action_form_support.createReaderActionFormFrame)({
	      document,
	      titleId,
	      title: request.title,
	      intro: request.intro,
	      closeDataAttribute: "data-report-close",
	      cancelDataAttribute: "data-report-cancel",
	      submitLabel: "提交举报",
	      renderIcon: this.#host.renderIcon
	    }), { form, status, submit } = frame, optionList = document.createElement("div");
	    optionList.className = "ldp-reader-action-options";
	    for (const [index, option] of options.entries()) {
	      const label = document.createElement("label");
	      label.className = "ldp-reader-action-option";
	      const radio = document.createElement("input");
	      radio.type = "radio", radio.name = `reader-report-type-${id}`, radio.value = String(option.id), radio.checked = index === 0;
	      const copy = document.createElement("span");
	      copy.className = "ldp-reader-action-option-copy";
	      const name = document.createElement("strong");
	      name.textContent = option.label;
	      const description = document.createElement("small");
	      description.textContent = option.description, copy.append(name, description), label.append(radio, copy), optionList.append(label);
	    }
	    const field = document.createElement("label");
	    field.className = "ldp-reader-action-field";
	    const fieldLabel = document.createElement("span");
	    fieldLabel.textContent = "补充说明";
	    const message = document.createElement("textarea");
	    message.maxLength = messageMaxLength, message.placeholder = request.placeholder ?? "选填;所选类型要求说明时必须填写", field.append(fieldLabel, message), status.before(optionList, field);
	    const session = this.#host.start({
	      frame,
	      previousFocus,
	      closeSelector: "[data-report-close]",
	      cancelSelector: "[data-report-cancel]"
	    });
	    return form.addEventListener("submit", (event) => {
	      if (event.preventDefault(), session.busy) return;
	      const selected = form.querySelector(
	        `input[name="reader-report-type-${id}"]:checked`
	      ), optionId = Number(selected?.value), option = options.find((entry) => entry.id === optionId), normalizedMessage = message.value.trim();
	      if (session.resetStatus(), !option) {
	        status.textContent = "请选择举报类型";
	        return;
	      }
	      if (option.requireMessage && !normalizedMessage) {
	        status.textContent = request.requiredMessageError ?? `${option.label}需要填写具体原因`, message.focus();
	        return;
	      }
	      session.submit({
	        execute: () => request.submit(Object.freeze({
	          optionId,
	          message: normalizedMessage
	        })),
	        setBusy: (busy) => (0, import_reader_action_form_support.setReaderActionFormBusy)(
	          form,
	          submit,
	          busy,
	          "提交中…",
	          "提交举报"
	        ),
	        successMessage: (message2) => message2 || "举报已提交",
	        failureMessage: (cause) => cause instanceof Error ? cause.message : "举报失败,请重试"
	      });
	    }), session.mount(() => {
	      optionList.querySelector(
	        'input[type="radio"]'
	      )?.focus({ preventScroll: !0 });
	    }), session.result;
	  }
	  destroy() {
	    this.#host.destroy();
	  }
	}
}, "f720bdf9c906620c34ce48021bcaadad9788f96c026a8fccd340dd307d3e51b3");

/* Source: lite/src/shell/reader-select-surface.ts */
runtime.register("src/shell/reader-select-surface.js", function(module, exports, require) {
	var reader_select_surface_exports = {};
	__export(reader_select_surface_exports, {
	  READER_SELECT_DISMISS_EVENT: () => READER_SELECT_DISMISS_EVENT,
	  READER_SELECT_RESELECT_EVENT: () => READER_SELECT_RESELECT_EVENT,
	  ReaderSelectSurface: () => ReaderSelectSurface
	});
	module.exports = __toCommonJS(reader_select_surface_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_event_target = require("../dom/event-target.js");
	const SELECTOR = [
	  "select.ldp-reader-select",
	  "select.ldp-cache-select",
	  "select.ldp-collection-scope",
	  "select.ldp-font-weight-select"
	].join(","), READER_SELECT_DISMISS_EVENT = "ldp-reader-select-dismiss", READER_SELECT_RESELECT_EVENT = "ldp-reader-select-reselect";
	function eventElement(event) {
	  const target = event.target;
	  return target && typeof target.closest == "function" ? target : null;
	}
	class ReaderSelectSurface {
	  scope;
	  #document;
	  #root;
	  #states = /* @__PURE__ */ new Map();
	  #openSelect = null;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#document = options.document, this.#root = options.root, this.#scan(options.root);
	    const Observer = options.document.defaultView?.MutationObserver ?? globalThis.MutationObserver;
	    if (typeof Observer == "function") {
	      const observer = new Observer((mutations) => {
	        for (const mutation of mutations)
	          for (const added of mutation.addedNodes) {
	            const element = added;
	            added.nodeType === 1 && this.#scan(element);
	          }
	        this.#prune();
	      });
	      this.scope.observe(observer, options.root, {
	        childList: !0,
	        subtree: !0
	      });
	    }
	    this.scope.listen(options.root, "pointerdown", (event) => this.#pointerDown(event)), this.scope.listen(options.root, "mousedown", (event) => this.#mouseDown(event)), this.scope.listen(options.root, "click", (event) => this.#click(event)), this.scope.listen(options.root, "keydown", (event) => this.#keydown(event)), this.scope.listen(options.root, "change", (event) => {
	      const select = eventElement(event)?.closest(SELECTOR);
	      select && this.#sync(select);
	    }), this.scope.listen(options.root, READER_SELECT_DISMISS_EVENT, () => {
	      this.#close();
	    }), this.scope.listen(options.document, "pointerdown", (event) => {
	      const select = this.#openSelect;
	      if (!select) return;
	      const wrapper = this.#states.get(select);
	      (0, import_event_target.eventPathIncludes)(event, wrapper ?? null) || this.#close();
	    }, !0);
	    const viewport = options.document.defaultView;
	    viewport && this.scope.listen(viewport, "resize", () => this.#positionOpenMenu());
	    for (const type of [
	      "ldp-reader-window-change",
	      "ldp-reader-workspace-change"
	    ])
	      this.scope.listen(options.root, type, () => this.#positionOpenMenu());
	    this.scope.add(() => {
	      this.#close();
	      for (const [select, wrapper] of this.#states)
	        select.removeAttribute("aria-expanded"), select.removeAttribute("aria-haspopup"), select.removeAttribute("data-reader-select-enhanced"), wrapper.replaceWith(select);
	      this.#states.clear();
	    });
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #scan(root) {
	    root.matches(SELECTOR) && this.#enhance(root);
	    for (const select of root.querySelectorAll(SELECTOR))
	      this.#enhance(select);
	  }
	  #enhance(select) {
	    if (select.multiple || select.dataset.readerSelectEnhanced || !select.parentNode) return;
	    const wrapper = this.#document.createElement("span");
	    wrapper.className = "ldp-select-surface";
	    const indicator = this.#document.createElement("span");
	    indicator.className = "ldp-select-indicator", indicator.setAttribute("aria-hidden", "true");
	    const menu = this.#document.createElement("span");
	    menu.className = "ldp-select-menu ldp-picker-options", menu.setAttribute("role", "presentation"), menu.hidden = !0, select.parentNode.insertBefore(wrapper, select), wrapper.append(select, indicator, menu), select.dataset.readerSelectEnhanced = "1", select.setAttribute("aria-haspopup", "listbox"), select.setAttribute("aria-expanded", "false"), this.#states.set(select, wrapper);
	  }
	  #prune() {
	    for (const [select] of this.#states)
	      select.isConnected || (this.#openSelect === select && (this.#openSelect = null), this.#states.delete(select));
	  }
	  #pointerDown(event) {
	    if (event.button !== 0) return;
	    const select = eventElement(event)?.closest(SELECTOR);
	    !select || !this.#root.contains(select) || select.disabled || (event.preventDefault(), select.focus({ preventScroll: !0 }), this.#openSelect === select ? this.#close() : this.#open(select));
	  }
	  #mouseDown(event) {
	    const select = eventElement(event)?.closest(SELECTOR);
	    select && this.#states.has(select) && event.preventDefault();
	  }
	  #click(event) {
	    const target = eventElement(event), option = target?.closest("[data-reader-select-value]");
	    if (option) {
	      const select2 = this.#openSelect;
	      if (!select2 || !this.#states.get(select2)?.contains(option) || (event.preventDefault(), option.disabled)) return;
	      const value = option.dataset.readerSelectValue ?? "", changed = select2.value !== value;
	      let matched = !1;
	      for (const nativeOption of select2.options) {
	        const selected = !matched && nativeOption.value === value;
	        nativeOption.selected = selected, selected && (matched = !0);
	      }
	      if (this.#sync(select2), this.#close(), select2.focus({ preventScroll: !0 }), changed) {
	        const EventConstructor = this.#document.defaultView?.Event ?? Event;
	        select2.dispatchEvent(new EventConstructor("input", { bubbles: !0 })), select2.dispatchEvent(new EventConstructor("change", { bubbles: !0 }));
	      } else {
	        const EventConstructor = this.#document.defaultView?.Event ?? Event;
	        select2.dispatchEvent(new EventConstructor(
	          READER_SELECT_RESELECT_EVENT,
	          { bubbles: !0 }
	        ));
	      }
	      return;
	    }
	    const select = target?.closest(SELECTOR);
	    select && this.#states.has(select) && event.preventDefault();
	  }
	  #keydown(event) {
	    const target = eventElement(event);
	    if (target?.closest(".ldp-select-search") && this.#openSelect) {
	      if (event.key === "Escape") {
	        event.preventDefault(), this.#close(!0);
	        return;
	      }
	      event.key === "ArrowDown" && (event.preventDefault(), this.#enabledOptions(this.#openSelect)[0]?.focus());
	      return;
	    }
	    const option = target?.closest("[data-reader-select-value]");
	    if (option && this.#openSelect) {
	      const buttons = this.#enabledOptions(this.#openSelect), index = buttons.indexOf(option);
	      if (event.key === "Escape") {
	        event.preventDefault(), this.#close(!0);
	        return;
	      }
	      if (event.key === "ArrowDown" || event.key === "ArrowUp") {
	        event.preventDefault();
	        const delta = event.key === "ArrowDown" ? 1 : -1;
	        buttons[(index + delta + buttons.length) % buttons.length]?.focus();
	      }
	      return;
	    }
	    const select = target?.closest(SELECTOR);
	    if (!(!select || !this.#states.has(select) || select.disabled)) {
	      if (event.key === "Escape" && this.#openSelect === select) {
	        event.preventDefault(), this.#close(!0);
	        return;
	      }
	      (["Enter", " ", "F4"].includes(event.key) || event.altKey && ["ArrowDown", "ArrowUp"].includes(event.key)) && (event.preventDefault(), this.#openSelect === select ? this.#close() : this.#open(select, !0));
	    }
	  }
	  #open(select, focusSelected = !1) {
	    this.#close();
	    const wrapper = this.#states.get(select), menu = wrapper?.querySelector(".ldp-select-menu");
	    if (!wrapper || !menu) return;
	    const options = this.#document.createElement("span");
	    if (options.className = "ldp-select-options", options.setAttribute("role", "listbox"), options.setAttribute(
	      "aria-label",
	      select.getAttribute("aria-label") || "下拉选项"
	    ), options.replaceChildren(...[...select.options].map((nativeOption) => {
	      const option = this.#document.createElement("button");
	      return option.type = "button", option.className = "ldp-select-option ldp-picker-option", option.dataset.readerSelectValue = nativeOption.value, option.textContent = nativeOption.label || nativeOption.textContent || "", option.disabled = nativeOption.disabled, option.hidden = nativeOption.hidden, option.setAttribute("role", "option"), option.setAttribute("aria-selected", String(nativeOption.selected)), option;
	    })), select.dataset.readerSelectSearchable === "true") {
	      const search2 = this.#document.createElement("input");
	      search2.type = "search", search2.className = "ldp-select-search";
	      const searchLabel = select.dataset.readerSelectSearchLabel ?? "搜索字体";
	      search2.placeholder = searchLabel, search2.setAttribute("aria-label", searchLabel);
	      const empty = this.#document.createElement("span");
	      empty.className = "ldp-select-empty", empty.textContent = select.dataset.readerSelectEmptyLabel ?? "没有匹配的字体", empty.hidden = !0, search2.addEventListener("input", () => {
	        const query = search2.value.trim().toLocaleLowerCase();
	        let visible = 0;
	        for (const option of options.querySelectorAll(
	          "[data-reader-select-value]"
	        )) {
	          const matches = !query || (option.textContent ?? "").toLocaleLowerCase().includes(query);
	          option.hidden = !matches, matches && (visible += 1);
	        }
	        empty.hidden = visible > 0;
	      }), menu.replaceChildren(search2, options, empty);
	    } else
	      menu.replaceChildren(options);
	    this.#openSelect = select, select.setAttribute("aria-expanded", "true"), menu.hidden = !1, this.#positionOpenMenu();
	    const search = menu.querySelector(".ldp-select-search");
	    search ? search.focus({ preventScroll: !0 }) : focusSelected && this.#enabledOptions(select).find((option) => option.dataset.readerSelectValue === select.value)?.focus();
	  }
	  #positionOpenMenu() {
	    const select = this.#openSelect, wrapper = select ? this.#states.get(select) : null, menu = wrapper?.querySelector(".ldp-select-menu"), viewport = this.#document.defaultView;
	    if (!select || !wrapper || !menu || menu.hidden || !viewport) return;
	    menu.style.removeProperty("left"), menu.style.removeProperty("max-height"), menu.style.removeProperty("max-width"), wrapper.classList.remove("is-menu-above");
	    const selectRect = select.getBoundingClientRect(), menuRect = menu.getBoundingClientRect(), menuWidth = menuRect.width || menu.offsetWidth, menuHeight = menuRect.height || menu.offsetHeight, margin = 12, gap = 6, settingsPanel = select.closest(".ldp-settings-panel"), collisionRect = (settingsPanel ?? select.closest(
	      ".ldp-unwanted-topic-filter-content,.ldp-settings-popover,.ldp-reader-floating-window,.ldp-notifications-popover,.ldp-history-popover,.ldp-bookmarks-popover,.ldp-topic-summary-surface"
	    ))?.getBoundingClientRect(), frozenIntroRect = (settingsPanel ? select.closest(".ldp-settings-section") : null)?.querySelector(".ldp-settings-intro")?.getBoundingClientRect(), bounds = Object.freeze({
	      left: Math.max(margin, (collisionRect?.left ?? 0) + margin),
	      right: Math.min(
	        viewport.innerWidth - margin,
	        (collisionRect?.right ?? viewport.innerWidth) - margin
	      ),
	      top: Math.max(
	        margin,
	        (collisionRect?.top ?? 0) + margin,
	        frozenIntroRect ? frozenIntroRect.bottom + margin : 0
	      ),
	      bottom: Math.min(
	        viewport.innerHeight - margin,
	        (collisionRect?.bottom ?? viewport.innerHeight) - margin
	      )
	    });
	    if (menuWidth > 0) {
	      const availableWidth = Math.max(1, bounds.right - bounds.left), positionedWidth = Math.min(menuWidth, availableWidth);
	      menuWidth > availableWidth && (menu.style.maxWidth = `${Math.floor(availableWidth)}px`);
	      const maxLeft = Math.max(bounds.left, bounds.right - positionedWidth), left = Math.max(
	        bounds.left,
	        Math.min(maxLeft, menuRect.left)
	      );
	      menu.style.left = `${Math.round(left - menuRect.left)}px`;
	    }
	    const spaceBelow = Math.max(
	      0,
	      bounds.bottom - selectRect.bottom - gap
	    ), spaceAbove = Math.max(
	      0,
	      selectRect.top - bounds.top - gap
	    ), menuAbove = menuHeight > spaceBelow && spaceAbove > spaceBelow;
	    wrapper.classList.toggle("is-menu-above", menuAbove);
	    const availableHeight = menuAbove ? spaceAbove : spaceBelow;
	    menuHeight > availableHeight && (menu.style.maxHeight = `${Math.max(1, Math.floor(availableHeight))}px`);
	  }
	  #sync(select) {
	    const wrapper = this.#states.get(select);
	    wrapper?.classList.toggle("is-disabled", select.disabled);
	    for (const option of wrapper?.querySelectorAll(
	      "[data-reader-select-value]"
	    ) ?? [])
	      option.setAttribute(
	        "aria-selected",
	        String(option.dataset.readerSelectValue === select.value)
	      );
	  }
	  #enabledOptions(select) {
	    return [...this.#states.get(select)?.querySelectorAll(
	      "[data-reader-select-value]:not(:disabled)"
	    ) ?? []].filter((option) => !option.hidden);
	  }
	  #close(restoreFocus = !1) {
	    const select = this.#openSelect;
	    if (!select) return;
	    this.#openSelect = null, select.setAttribute("aria-expanded", "false");
	    const wrapper = this.#states.get(select);
	    wrapper?.classList.remove("is-menu-above");
	    const menu = wrapper?.querySelector(".ldp-select-menu");
	    menu && (menu.hidden = !0, menu.style.removeProperty("left"), menu.style.removeProperty("max-height"), menu.style.removeProperty("max-width")), restoreFocus && select.focus({ preventScroll: !0 });
	  }
	}
}, "a316312b868e84deede49135f4c40fc6af6b415272cf9ab806c313e391ee3067");

/* Source: lite/src/shell/reader-shell-recovery-view.ts */
runtime.register("src/shell/reader-shell-recovery-view.js", function(module, exports, require) {
	var reader_shell_recovery_view_exports = {};
	__export(reader_shell_recovery_view_exports, {
	  ReaderShellRecoveryView: () => ReaderShellRecoveryView
	});
	module.exports = __toCommonJS(reader_shell_recovery_view_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderShellRecoveryView {
	  scope;
	  #host;
	  #root;
	  #message;
	  #detail;
	  #retry;
	  #challenge;
	  #onRetry;
	  #onClose;
	  #busy = !1;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#host = options.host, this.#onRetry = options.onRetry, this.#onClose = options.onClose;
	    const document = options.document;
	    this.#root = document.createElement("div"), this.#root.className = "ldp-error", this.#root.role = "alert", this.#root.hidden = !0, this.#message = document.createElement("div"), this.#message.className = "ldp-error-message", this.#detail = document.createElement("small"), this.#detail.className = "ldp-error-message ldp-error-detail";
	    const actions = document.createElement("div");
	    actions.className = "ldp-error-actions", this.#retry = document.createElement("button"), this.#retry.className = "ldp-error-retry", this.#retry.type = "button", this.#retry.textContent = "重新加载", this.#challenge = document.createElement("a"), this.#challenge.className = "ldp-error-challenge", this.#challenge.target = "_blank", this.#challenge.rel = "noopener noreferrer", this.#challenge.textContent = "手动完成 Cloudflare 验证", this.#challenge.hidden = !0;
	    const close = document.createElement("button");
	    close.className = "ldp-error-close", close.type = "button", close.textContent = "关闭阅读器", actions.append(this.#retry, this.#challenge, close), this.#root.append(this.#message, this.#detail, actions), this.scope.listen(this.#retry, "click", () => void this.#retryNow()), this.scope.listen(close, "click", () => void this.#close()), this.scope.add(() => this.#root.remove());
	  }
	  get visible() {
	    return !this.#root.hidden && this.#root.isConnected;
	  }
	  show(failure) {
	    this.scope.destroyed || (this.#message.textContent = failure.message, this.#detail.textContent = failure.detail, this.#root.dataset.failureKind = failure.kind, failure.kind === "cloudflare" && failure.challengeHref ? (this.#challenge.href = failure.challengeHref, this.#challenge.hidden = !1) : (this.#challenge.removeAttribute("href"), this.#challenge.hidden = !0), this.#busy = !1, this.#retry.disabled = !1, this.#retry.textContent = "重新加载", this.#root.hidden = !1, this.#root.parentNode !== this.#host && this.#host.append(this.#root));
	  }
	  clear() {
	    this.#busy = !1, this.#retry.disabled = !1, this.#retry.textContent = "重新加载", this.#root.hidden = !0, this.#root.remove();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #retryNow() {
	    if (!(this.#busy || this.scope.destroyed)) {
	      this.#busy = !0, this.#retry.disabled = !0, this.#retry.textContent = "正在重新加载…";
	      try {
	        await this.#onRetry() && this.clear();
	      } catch {
	        this.visible && (this.#detail.textContent = "重新加载仍然失败;当前状态已保留,请稍后再试。");
	      } finally {
	        this.#busy = !1, this.visible && (this.#retry.disabled = !1, this.#retry.textContent = "重新加载");
	      }
	    }
	  }
	  async #close() {
	    this.scope.destroyed || (this.clear(), await this.#onClose());
	  }
	}
}, "3cdf02d3208fe62efdafc48ed1193199225cf205f36b1a83022857526825921c");

/* Source: lite/src/shell/reader-shell-template.ts */
runtime.register("src/shell/reader-shell-template.js", function(module, exports, require) {
	var reader_shell_template_exports = {};
	__export(reader_shell_template_exports, {
	  createReaderShellTemplate: () => createReaderShellTemplate
	});
	module.exports = __toCommonJS(reader_shell_template_exports);
	var import_reader_notification_model = require("../notification/reader-notification-model.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_image_fallback = require("../components/reader-image-fallback.js"), import_html_element = require("../dom/html-element.js"), import_reader_popover_filter_controls = require("../collection/reader-popover-filter-controls.js");
	function nonEmpty(value, name) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error(`${name} 不能为空`);
	  return normalized;
	}
	function icon(options, name) {
	  return (0, import_reader_icon.renderReaderIcon)(options.document, name, options.renderIcon);
	}
	function button(options, className, label, iconName) {
	  const node = (0, import_html_element.htmlElement)(options.document, "button", className);
	  return node.type = "button", node.setAttribute("aria-label", label), node.append(icon(options, iconName)), node;
	}
	function popoverPager(options, name, infoText) {
	  const root = (0, import_html_element.htmlElement)(
	    options.document,
	    "div",
	    "ldp-notification-pager"
	  ), prefix = name === "notification" ? "" : `ldp-${name}-page-`, previous = button(
	    options,
	    `${prefix ? `${prefix}prev ` : ""}ldp-notification-page-prev`,
	    "上一页",
	    "chevron-left"
	  );
	  previous.disabled = !0;
	  const info = (0, import_html_element.htmlElement)(
	    options.document,
	    "span",
	    `${prefix ? `${prefix}info ` : ""}ldp-notification-page-info`
	  );
	  info.textContent = infoText;
	  const next = button(
	    options,
	    `${prefix ? `${prefix}next ` : ""}ldp-notification-page-next`,
	    "下一页",
	    "chevron-right"
	  );
	  return next.disabled = !0, root.append(previous, info, next), { root, previous, info, next };
	}
	function collectionBulkActions(options, name, noun, deleteLabel) {
	  const root = (0, import_html_element.htmlElement)(
	    options.document,
	    "div",
	    `ldp-collection-title-actions ldp-${name}-bulk-actions`
	  );
	  root.hidden = !0;
	  const scope = (0, import_html_element.htmlElement)(
	    options.document,
	    "select",
	    `ldp-reader-select ldp-collection-scope ldp-${name}-select-scope`
	  );
	  scope.setAttribute(
	    "aria-label",
	    name === "history" ? "历史全选范围" : "收藏全选范围"
	  );
	  for (const [value, label] of [
	    ["page", "已加载"],
	    ["all", "全部记录"]
	  ]) {
	    const option = (0, import_html_element.htmlElement)(options.document, "option", "");
	    option.value = value, option.textContent = label, scope.append(option);
	  }
	  const select = button(
	    options,
	    `ldp-collection-action ldp-${name}-select-toggle`,
	    `全选已加载${noun}`,
	    name === "bookmarks" ? "select-items" : "square"
	  );
	  select.setAttribute("aria-pressed", "false");
	  const remove = button(
	    options,
	    `ldp-collection-action danger ldp-${name}-delete-selected`,
	    deleteLabel,
	    name === "bookmarks" ? "trash-2" : "trash"
	  );
	  remove.disabled = !0;
	  const count = (0, import_html_element.htmlElement)(
	    options.document,
	    "b",
	    `ldp-collection-count ldp-${name}-delete-selected-label`
	  );
	  count.hidden = !0, count.textContent = "0", remove.append(count);
	  const done = button(
	    options,
	    `ldp-collection-action ldp-${name}-multi-done`,
	    "退出多选",
	    name === "bookmarks" ? "check" : "x"
	  );
	  return root.append(scope, select, remove, done), { root, scope, select, remove, count, done };
	}
	function popoverList(document, className, emptyText) {
	  const list = (0, import_html_element.htmlElement)(document, "div", className), empty = (0, import_html_element.htmlElement)(document, "div", "ldp-notification-empty");
	  return empty.textContent = emptyText, list.append(empty), list;
	}
	function createReaderShellTemplate(options) {
	  const { document } = options, siteName = nonEmpty(options.siteName, "siteName"), homeUrl = nonEmpty(options.homeUrl, "homeUrl"), root = (0, import_html_element.htmlElement)(document, "div", "ldp-overlay");
	  root.dataset.readerWorkspaceMode = "floating", root.dataset.readerListModeAllowed = String(options.listModeAllowed);
	  const capsule = (0, import_html_element.htmlElement)(document, "div", "ldp-reader-window-capsule"), pinButton = button(
	    options,
	    "ldp-reader-pin-button",
	    "点击外部时保持显示",
	    "pin"
	  );
	  pinButton.setAttribute("aria-pressed", "false");
	  const lockButton = (0, import_html_element.htmlElement)(document, "button", "ldp-reader-lock-button");
	  lockButton.type = "button", lockButton.setAttribute("aria-label", "锁定浮窗");
	  const unlockedIcon = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-lock-icon");
	  unlockedIcon.dataset.readerLockIcon = "unlocked", unlockedIcon.append(icon(options, "unlock"));
	  const lockedIcon = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-lock-icon");
	  lockedIcon.dataset.readerLockIcon = "locked", lockedIcon.hidden = !0, lockedIcon.append(icon(options, "lock")), lockButton.append(unlockedIcon, lockedIcon), lockButton.setAttribute("aria-pressed", "false");
	  const placementControl = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reader-placement-control"
	  ), placementDivider = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-reader-placement-divider"
	  );
	  placementDivider.setAttribute("aria-hidden", "true");
	  const placementStrip = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reader-placement-strip"
	  );
	  placementStrip.setAttribute("role", "group"), placementStrip.setAttribute("aria-label", "切换阅读器显示方式");
	  const placementOptions = Object.freeze([
	    ["embed-left", "嵌入左侧", "panel-left"],
	    ["embed-right", "嵌入右侧", "panel-right"],
	    ["floating", "浮窗阅读器", "floating-window"],
	    ["fullpage", "全屏阅读器", "maximize-2"]
	  ].map(([mode, label, iconName]) => {
	    const option = button(
	      options,
	      "ldp-reader-placement-option",
	      label,
	      iconName
	    );
	    return option.dataset.readerPlacement = mode, option.dataset.tooltip = "", option.setAttribute("aria-pressed", "false"), placementStrip.append(option), option;
	  }));
	  placementControl.append(placementDivider, placementStrip), capsule.append(pinButton, lockButton, placementControl);
	  const hostScrollbar = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reader-host-scrollbar ldp-reader-host-scrollbar-inactive"
	  );
	  hostScrollbar.setAttribute("role", "scrollbar"), hostScrollbar.setAttribute("aria-label", "原站主题列表滚动条"), hostScrollbar.setAttribute("aria-orientation", "vertical"), hostScrollbar.setAttribute("aria-valuemin", "0"), hostScrollbar.setAttribute("aria-valuemax", "0"), hostScrollbar.setAttribute("aria-valuenow", "0"), hostScrollbar.setAttribute("aria-disabled", "true"), hostScrollbar.tabIndex = 0;
	  const hostScrollbarThumb = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-reader-host-scrollbar-thumb"
	  );
	  hostScrollbarThumb.setAttribute("aria-hidden", "true"), hostScrollbar.append(hostScrollbarThumb);
	  const hostTopButton = button(
	    options,
	    "ldp-reader-host-top",
	    "回到原站页面顶部",
	    "arrow-up"
	  );
	  hostTopButton.title = "回到原站页面顶部", hostTopButton.hidden = !0;
	  const countdown = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-reader-host-top-countdown"
	  );
	  countdown.textContent = "3", countdown.setAttribute("aria-hidden", "true"), hostTopButton.append(countdown);
	  const modal = (0, import_html_element.htmlElement)(document, "div", "ldp-modal"), embedResizeHandle = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-reader-embed-resize"
	  );
	  embedResizeHandle.setAttribute("aria-hidden", "true"), modal.append(embedResizeHandle);
	  for (const direction of ["n", "s", "e", "w", "nw", "ne", "sw", "se"]) {
	    const handle = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-resize-handle");
	    handle.dataset.readerResize = direction, handle.setAttribute("aria-hidden", "true"), modal.append(handle);
	  }
	  const header = (0, import_html_element.htmlElement)(document, "div", "ldp-header"), home = (0, import_html_element.htmlElement)(document, "a", "ldp-home-logo");
	  if (home.href = homeUrl, home.setAttribute("aria-label", `回到 ${siteName} 首页`), options.logoUrl) {
	    const logo = (0, import_html_element.htmlElement)(document, "img", "ldp-logo");
	    (0, import_reader_image_fallback.installReaderSiteLogoFallback)(logo, options.logoUrl), logo.alt = "", logo.loading = "lazy", logo.decoding = "async", logo.dataset.ldpSiteLogo = "", home.append(logo);
	  }
	  const titleWrap = (0, import_html_element.htmlElement)(document, "div", "ldp-title-wrap"), title = (0, import_html_element.htmlElement)(document, "h2", "ldp-title"), titleJump = (0, import_html_element.htmlElement)(document, "span", "ldp-title-jump");
	  titleJump.textContent = options.loadingTitle ?? "正在载入主题…", titleJump.setAttribute("role", "button"), titleJump.tabIndex = 0, titleJump.setAttribute("aria-label", "跳到 #1");
	  const topicEditTrigger = button(
	    options,
	    "ldp-topic-edit-trigger",
	    "编辑帖子标题、类别和 label",
	    "pencil"
	  );
	  topicEditTrigger.hidden = !0, topicEditTrigger.setAttribute("aria-haspopup", "dialog"), topicEditTrigger.setAttribute("aria-expanded", "false"), title.append(titleJump, topicEditTrigger);
	  const titleSubline = (0, import_html_element.htmlElement)(document, "div", "ldp-title-subline"), metaRow = (0, import_html_element.htmlElement)(document, "div", "ldp-meta-row"), meta = (0, import_html_element.htmlElement)(document, "div", "ldp-meta"), metaStats = (0, import_html_element.htmlElement)(document, "span", "ldp-meta-stats");
	  metaStats.textContent = "正在读取主题信息…";
	  const metaOwner = (0, import_html_element.htmlElement)(document, "span", "ldp-meta-owner");
	  metaOwner.hidden = !0;
	  const metaOwnerCopy = (0, import_html_element.htmlElement)(document, "span", "ldp-meta-owner-copy");
	  metaOwnerCopy.append("楼主 ");
	  const metaOwnerValue = (0, import_html_element.htmlElement)(
	    document,
	    "a",
	    "ldp-user-link ldp-topic-owner-link ldp-meta-owner-value"
	  );
	  metaOwnerCopy.append(metaOwnerValue);
	  const onlyOpToggle = (0, import_html_element.htmlElement)(
	    document,
	    "button",
	    "ldp-only-op-toggle"
	  );
	  onlyOpToggle.type = "button", onlyOpToggle.disabled = !0, onlyOpToggle.setAttribute("aria-label", "只看楼主"), onlyOpToggle.setAttribute("aria-pressed", "false"), onlyOpToggle.append(icon(options, "user-round")), metaOwner.append(metaOwnerCopy, onlyOpToggle);
	  const onlyOpProgress = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-only-op-progress"
	  );
	  onlyOpProgress.hidden = !0, onlyOpProgress.setAttribute("role", "status"), onlyOpProgress.setAttribute("aria-live", "polite");
	  const onlyOpProgressTrack = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-only-op-progress-track"
	  ), onlyOpProgressFill = (0, import_html_element.htmlElement)(
	    document,
	    "i",
	    "ldp-only-op-progress-fill"
	  );
	  onlyOpProgressTrack.append(onlyOpProgressFill);
	  const onlyOpProgressValue = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-only-op-progress-value"
	  );
	  onlyOpProgress.append(onlyOpProgressTrack, onlyOpProgressValue), meta.append(metaStats, metaOwner, onlyOpProgress), metaRow.append(meta);
	  const topicIdentityHost = (0, import_html_element.htmlElement)(document, "div", "ldp-title-topic-row");
	  titleSubline.append(metaRow, topicIdentityHost), titleWrap.append(title, titleSubline);
	  const headerActions = (0, import_html_element.htmlElement)(document, "div", "ldp-head-btns"), layoutToggle = button(
	    options,
	    "ldp-layout-toggle",
	    "切换阅读器布局",
	    "maximize-2"
	  ), notificationsToggle = button(
	    options,
	    "ldp-notifications-toggle",
	    "消息",
	    "bell"
	  );
	  notificationsToggle.setAttribute("aria-expanded", "false");
	  const notificationsToggleLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  notificationsToggleLabel.textContent = "消息";
	  const notificationUnreadBadge = (0, import_html_element.htmlElement)(
	    document,
	    "b",
	    "ldp-notification-unread-badge"
	  );
	  notificationUnreadBadge.hidden = !0, notificationsToggle.append(
	    notificationsToggleLabel,
	    notificationUnreadBadge
	  );
	  const notificationsPopover = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-notifications-popover"
	  );
	  notificationsPopover.hidden = !0;
	  const notificationModeTabsHost = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-notification-mode-tabs"
	  );
	  notificationModeTabsHost.setAttribute("role", "tablist"), notificationModeTabsHost.setAttribute("aria-label", "消息来源");
	  const notificationModeTabs = [];
	  for (const [mode, label, iconName] of [
	    ["notifications", "通知", "bell"],
	    ["messages", "私信", "mail"]
	  ]) {
	    const tab = button(
	      options,
	      "ldp-notification-mode-tab",
	      label,
	      iconName
	    );
	    tab.dataset.notificationMode = mode, tab.setAttribute("role", "tab");
	    const active = mode === "notifications";
	    tab.classList.toggle("active", active), tab.setAttribute("aria-selected", String(active));
	    const copy = (0, import_html_element.htmlElement)(document, "span", "");
	    copy.textContent = label, tab.append(copy), notificationModeTabs.push(tab), notificationModeTabsHost.append(tab);
	  }
	  const notificationGroupPanels = [], notificationGroupTabs = [];
	  for (const mode of ["notifications", "messages"]) {
	    const keys = import_reader_notification_model.READER_NOTIFICATION_PANEL_GROUP_ORDER.filter((key) => import_reader_notification_model.READER_NOTIFICATION_GROUPS[key].mode === mode), panel = (0, import_html_element.htmlElement)(document, "div", "ldp-notification-tabs");
	    panel.dataset.notificationModePanel = mode, panel.setAttribute("role", "tablist"), panel.setAttribute("aria-label", mode === "notifications" ? "通知分类" : "私信分类"), panel.style.setProperty("--ldp-notification-tab-count", String(keys.length)), panel.hidden = mode === "messages";
	    for (const key of keys) {
	      const group = import_reader_notification_model.READER_NOTIFICATION_GROUPS[key], tab = button(
	        options,
	        "ldp-notification-tab",
	        group.label,
	        group.icon
	      );
	      tab.dataset.notificationGroup = key, tab.setAttribute("role", "tab");
	      const active = key === "all";
	      tab.classList.toggle("active", active), tab.setAttribute("aria-selected", String(active));
	      const copy = (0, import_html_element.htmlElement)(document, "span", "");
	      copy.textContent = group.label, tab.append(copy), notificationGroupTabs.push(tab), panel.append(tab);
	    }
	    notificationGroupPanels.push(panel);
	  }
	  const notificationToolbar = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-notification-toolbar"
	  ), notificationUnreadStatus = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-notification-unread-status"
	  );
	  notificationUnreadStatus.textContent = "没有未读消息";
	  const notificationMarkAll = button(
	    options,
	    "ldp-notification-mark-all",
	    "全部已读",
	    "check-square"
	  );
	  notificationMarkAll.disabled = !0;
	  const notificationMarkAllLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  notificationMarkAllLabel.textContent = "全部已读", notificationMarkAll.append(notificationMarkAllLabel);
	  const notificationNewMessage = (0, import_html_element.htmlElement)(
	    document,
	    "a",
	    "ldp-notification-new-message"
	  );
	  notificationNewMessage.href = `${homeUrl.replace(/\/+$/, "")}/new-message` || "/new-message", notificationNewMessage.hidden = !0, notificationNewMessage.append(icon(options, "mail"));
	  const notificationNewMessageLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  notificationNewMessageLabel.textContent = "新消息", notificationNewMessage.append(notificationNewMessageLabel), notificationToolbar.append(
	    notificationUnreadStatus,
	    notificationMarkAll,
	    notificationNewMessage
	  );
	  const notificationSearchTools = (0, import_reader_popover_filter_controls.createReaderPopoverSearchTools)(
	    document,
	    "notification",
	    "搜索用户、标题、内容或拼音",
	    "搜索消息",
	    "清空消息搜索",
	    options.renderIcon
	  ), notificationSearch = notificationSearchTools.search.input, notificationSearchClear = notificationSearchTools.search.clear, notificationCategoryFilter = notificationSearchTools.category, notificationTagFilter = notificationSearchTools.tag, notificationList = popoverList(
	    document,
	    "ldp-notification-list",
	    "正在加载消息…"
	  ), {
	    root: notificationPager,
	    previous: notificationPagePrevious,
	    info: notificationPageInfo,
	    next: notificationPageNext
	  } = popoverPager(
	    options,
	    "notification",
	    "第 1 页"
	  );
	  notificationsPopover.append(
	    notificationModeTabsHost,
	    ...notificationGroupPanels,
	    notificationToolbar,
	    notificationSearchTools.root,
	    notificationList,
	    notificationPager
	  );
	  const historyToggle = button(
	    options,
	    "ldp-history-toggle",
	    "浏览历史",
	    "history"
	  );
	  historyToggle.setAttribute("aria-expanded", "false");
	  const historyPopover = (0, import_html_element.htmlElement)(document, "div", "ldp-history-popover");
	  historyPopover.hidden = !0;
	  const historyTitle = (0, import_html_element.htmlElement)(document, "div", "ldp-collection-title"), historyTitleLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  historyTitleLabel.textContent = "浏览历史";
	  const historyDefaultActions = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-collection-title-actions ldp-history-default-actions"
	  ), historySortToggle = button(
	    options,
	    "ldp-collection-action ldp-history-sort-toggle",
	    "切换浏览历史排序",
	    "history"
	  ), historyMultiButton = button(
	    options,
	    "ldp-collection-action ldp-history-multi",
	    "多选浏览历史",
	    "list-checks"
	  ), historyClearButton = button(
	    options,
	    "ldp-collection-action danger ldp-history-clear",
	    "清空全部浏览历史",
	    "trash"
	  );
	  historyClearButton.disabled = !0, historyDefaultActions.append(
	    historySortToggle,
	    historyMultiButton,
	    historyClearButton
	  );
	  const {
	    root: historyBulkActions,
	    scope: historySelectScope,
	    select: historySelectToggle,
	    remove: historyDeleteSelected,
	    count: historyDeleteSelectedLabel,
	    done: historyMultiDone
	  } = collectionBulkActions(
	    options,
	    "history",
	    "浏览历史",
	    "删除所选浏览历史"
	  );
	  historyTitle.append(
	    historyTitleLabel,
	    historyDefaultActions,
	    historyBulkActions
	  );
	  const historySearchTools = (0, import_reader_popover_filter_controls.createReaderPopoverSearchTools)(
	    document,
	    "history",
	    "搜索标题或拼音",
	    "搜索浏览历史",
	    "清空历史搜索",
	    options.renderIcon
	  ), historySearch = historySearchTools.search.input, historySearchClear = historySearchTools.search.clear, historyCategoryFilter = historySearchTools.category, historyTagFilter = historySearchTools.tag, historyList = popoverList(
	    document,
	    "ldp-history-list ldp-notification-list",
	    "暂无浏览历史"
	  ), {
	    root: historyPager,
	    previous: historyPagePrevious,
	    info: historyPageInfo,
	    next: historyPageNext
	  } = popoverPager(
	    options,
	    "history",
	    "暂无记录"
	  );
	  historyPopover.append(
	    historyTitle,
	    historySearchTools.root,
	    historyList,
	    historyPager
	  );
	  const bookmarksToggle = button(
	    options,
	    "ldp-bookmarks-toggle",
	    "收藏与回应",
	    "bookmark"
	  );
	  bookmarksToggle.setAttribute("aria-expanded", "false");
	  const bookmarksToggleLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  bookmarksToggleLabel.textContent = "收藏", bookmarksToggle.append(bookmarksToggleLabel);
	  const bookmarksPopover = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-bookmarks-popover"
	  );
	  bookmarksPopover.hidden = !0;
	  const bookmarksTitle = (0, import_html_element.htmlElement)(document, "div", "ldp-collection-title"), bookmarksTitleLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  bookmarksTitleLabel.textContent = "收藏与回应";
	  const bookmarksDefaultActions = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-collection-title-actions ldp-bookmarks-default-actions"
	  ), bookmarksMultiButton = button(
	    options,
	    "ldp-collection-action ldp-bookmarks-multi",
	    "多选收藏",
	    "list-checks"
	  );
	  bookmarksDefaultActions.append(bookmarksMultiButton);
	  const {
	    root: bookmarksBulkActions,
	    scope: bookmarksSelectScope,
	    select: bookmarksSelectToggle,
	    remove: bookmarksDeleteSelected,
	    count: bookmarksDeleteSelectedLabel,
	    done: bookmarksMultiDone
	  } = collectionBulkActions(
	    options,
	    "bookmarks",
	    "收藏",
	    "取消所选收藏"
	  );
	  bookmarksTitle.append(
	    bookmarksTitleLabel,
	    bookmarksDefaultActions,
	    bookmarksBulkActions
	  );
	  const bookmarkTabsHost = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-bookmark-tabs"
	  );
	  bookmarkTabsHost.setAttribute("role", "tablist"), bookmarkTabsHost.setAttribute("aria-label", "收藏与回应类型");
	  const bookmarkTabs = [];
	  for (const [type, label] of [
	    ["Reply", "回复"],
	    ["Boost", "Boost"],
	    ["Reaction", "表情回应"],
	    ["Topic", "收藏帖子"],
	    ["Post", "收藏楼层"]
	  ]) {
	    const tab = button(
	      options,
	      "ldp-bookmark-tab",
	      `${label};拖动排序,首项默认`,
	      "bookmark"
	    );
	    tab.dataset.bookmarkType = type, tab.setAttribute("role", "tab"), tab.setAttribute("aria-selected", String(type === "Reply")), tab.classList.toggle("active", type === "Reply"), tab.replaceChildren(), tab.textContent = label, bookmarkTabs.push(tab), bookmarkTabsHost.append(tab);
	  }
	  const bookmarksSearchTools = (0, import_reader_popover_filter_controls.createReaderPopoverSearchTools)(
	    document,
	    "bookmarks",
	    "搜索收藏标题、内容或拼音",
	    "搜索收藏",
	    "清空收藏搜索",
	    options.renderIcon
	  ), bookmarksSearch = bookmarksSearchTools.search.input, bookmarksSearchClear = bookmarksSearchTools.search.clear, bookmarkCategoryFilter = bookmarksSearchTools.category, bookmarkTagFilter = bookmarksSearchTools.tag, bookmarkReactionFilters = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reaction-filters"
	  );
	  bookmarkReactionFilters.setAttribute("role", "group"), bookmarkReactionFilters.setAttribute("aria-label", "按回应表情筛选"), bookmarkReactionFilters.hidden = !0;
	  const bookmarksList = popoverList(
	    document,
	    "ldp-bookmarks-list ldp-notification-list",
	    "正在加载收藏…"
	  ), {
	    root: bookmarksPager,
	    previous: bookmarksPagePrevious,
	    info: bookmarksPageInfo,
	    next: bookmarksPageNext
	  } = popoverPager(
	    options,
	    "bookmarks",
	    "暂无记录"
	  );
	  bookmarksPopover.append(
	    bookmarksTitle,
	    bookmarkTabsHost,
	    bookmarksSearchTools.root,
	    bookmarkReactionFilters,
	    bookmarksList,
	    bookmarksPager
	  );
	  const openNative = (0, import_html_element.htmlElement)(document, "a", "ldp-open ldp-icon-btn");
	  openNative.target = "_blank", openNative.rel = "noopener noreferrer", openNative.setAttribute("aria-label", "打开原生主题页面"), openNative.title = "打开原生主题页面", openNative.hidden = !0, openNative.append(icon(options, "external-link")), headerActions.append(
	    layoutToggle,
	    notificationsToggle,
	    notificationsPopover,
	    historyToggle,
	    historyPopover,
	    bookmarksToggle,
	    bookmarksPopover
	  );
	  const titleActions = (0, import_html_element.htmlElement)(document, "div", "ldp-title-actions"), headerActionsToggle = button(
	    options,
	    "ldp-header-actions-toggle",
	    "展开其余标题栏操作",
	    "chevron-left"
	  );
	  headerActionsToggle.setAttribute("aria-expanded", "false");
	  const refreshTopic = button(
	    options,
	    "ldp-reader-refresh ldp-icon-btn",
	    "清除当前帖子缓存并刷新",
	    "rotate-ccw"
	  );
	  refreshTopic.disabled = !0;
	  const closeReader = button(
	    options,
	    "ldp-close ldp-icon-btn",
	    "关闭阅读器",
	    "x"
	  ), setHeaderActionsExpanded = (expanded) => {
	    titleActions.classList.toggle("is-expanded", expanded), header.classList.toggle("ldp-title-actions-expanded", expanded), headerActionsToggle.setAttribute("aria-expanded", String(expanded));
	    const label = expanded ? "收起原右上角操作" : "展开原右上角操作";
	    headerActionsToggle.setAttribute("aria-label", label), headerActionsToggle.title = label, headerActionsToggle.replaceChildren(
	      icon(options, expanded ? "chevron-right" : "chevron-left")
	    );
	  };
	  titleActions.addEventListener("pointerenter", () => {
	    setHeaderActionsExpanded(!0);
	  }), titleActions.addEventListener("pointerleave", () => {
	    setHeaderActionsExpanded(!1);
	  }), titleActions.addEventListener("focusin", () => {
	    setHeaderActionsExpanded(!0);
	  }), titleActions.addEventListener("focusout", (event) => {
	    const next = event.relatedTarget;
	    (!next || !titleActions.contains(next)) && setHeaderActionsExpanded(!1);
	  }), headerActionsToggle.addEventListener("click", () => {
	    setHeaderActionsExpanded(!0);
	  }), setHeaderActionsExpanded(!1), titleActions.append(headerActionsToggle, refreshTopic, openNative, closeReader), header.append(home, titleWrap, headerActions, titleActions);
	  const readerMain = (0, import_html_element.htmlElement)(document, "div", "ldp-reader-main"), rateLimitNotice = (0, import_html_element.htmlElement)(document, "div", "ldp-rate-limit-notice");
	  rateLimitNotice.hidden = !0, rateLimitNotice.setAttribute("role", "status"), rateLimitNotice.setAttribute("aria-live", "polite"), rateLimitNotice.setAttribute("aria-atomic", "true");
	  const rateLimitIcon = (0, import_html_element.htmlElement)(document, "span", "ldp-rate-limit-icon");
	  rateLimitIcon.setAttribute("aria-hidden", "true"), rateLimitIcon.append(icon(options, "alert-triangle"));
	  const rateLimitCopy = (0, import_html_element.htmlElement)(document, "span", "ldp-rate-limit-copy"), rateLimitTitle = (0, import_html_element.htmlElement)(document, "strong", "");
	  rateLimitTitle.textContent = "Cloudflare 验证";
	  const rateLimitDetail = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-rate-limit-detail"
	  );
	  rateLimitDetail.textContent = "关键 Reader 请求遇到 Cloudflare 验证时暂停新请求,最多只打开一个独立过盾浮窗。", rateLimitCopy.append(rateLimitTitle, rateLimitDetail);
	  const rateLimitChallenge = (0, import_html_element.htmlElement)(
	    document,
	    "a",
	    "ldp-rate-limit-challenge"
	  );
	  rateLimitChallenge.target = "_blank", rateLimitChallenge.rel = "noopener", rateLimitChallenge.setAttribute(
	    "aria-label",
	    "在独立浮窗打开 LINUX DO 官网完成 Cloudflare 验证"
	  ), rateLimitChallenge.append(icon(options, "external-link"));
	  const rateLimitChallengeLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  rateLimitChallengeLabel.textContent = "打开过盾浮窗", rateLimitChallenge.append(rateLimitChallengeLabel), rateLimitNotice.append(
	    rateLimitIcon,
	    rateLimitCopy,
	    rateLimitChallenge
	  );
	  const body = (0, import_html_element.htmlElement)(document, "div", "ldp-body"), topicTimeline = (0, import_html_element.htmlElement)(document, "aside", "ldp-topic-timeline");
	  topicTimeline.hidden = !0, topicTimeline.setAttribute("aria-label", "帖子时间轴");
	  const topicTimelineDate = (0, import_html_element.htmlElement)(
	    document,
	    "button",
	    "ldp-topic-timeline-date"
	  );
	  topicTimelineDate.type = "button", topicTimelineDate.setAttribute("aria-label", "跳到首帖");
	  const topicTimelineTrack = (0, import_html_element.htmlElement)(
	    document,
	    "button",
	    "ldp-topic-timeline-track"
	  );
	  topicTimelineTrack.type = "button", topicTimelineTrack.setAttribute("role", "slider"), topicTimelineTrack.setAttribute("aria-label", "跳转楼层"), topicTimelineTrack.setAttribute("aria-orientation", "vertical");
	  const topicTimelineCursor = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-cursor ldp-timeline-lens-composited"
	  );
	  topicTimelineCursor.setAttribute("aria-hidden", "true");
	  const topicTimelineThumb = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-thumb"
	  ), topicTimelineCount = (0, import_html_element.htmlElement)(
	    document,
	    "strong",
	    "ldp-topic-timeline-count"
	  ), topicTimelineCurrent = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-current"
	  );
	  topicTimelineCurrent.textContent = "1";
	  const topicTimelineDivider = (0, import_html_element.htmlElement)(document, "span", "");
	  topicTimelineDivider.textContent = "/";
	  const topicTimelineTotal = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-total"
	  );
	  topicTimelineTotal.textContent = "1", topicTimelineCount.append(
	    topicTimelineCurrent,
	    topicTimelineDivider,
	    topicTimelineTotal
	  );
	  const topicTimelinePreview = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-preview"
	  );
	  topicTimelinePreview.setAttribute("aria-hidden", "true"), topicTimelineTrack.append(
	    topicTimelineCursor,
	    topicTimelineThumb,
	    topicTimelineCount,
	    topicTimelinePreview
	  );
	  const topicTimelineFooter = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-topic-timeline-footer"
	  ), topicTimelineRelative = (0, import_html_element.htmlElement)(
	    document,
	    "button",
	    "ldp-topic-timeline-relative"
	  );
	  topicTimelineRelative.type = "button", topicTimelineRelative.setAttribute("aria-label", "跳到最新楼层");
	  const topicTimelineJump = (0, import_html_element.htmlElement)(
	    document,
	    "button",
	    "ldp-topic-timeline-jump"
	  );
	  topicTimelineJump.type = "button", topicTimelineJump.textContent = "#", topicTimelineJump.setAttribute("aria-label", "跳到指定楼层"), topicTimelineJump.setAttribute("aria-haspopup", "dialog"), topicTimelineJump.setAttribute("aria-expanded", "false");
	  const topicTimelineTop = button(
	    options,
	    "ldp-topic-timeline-top",
	    "回到顶部,第 1 楼",
	    "arrow-up"
	  );
	  topicTimelineFooter.append(
	    topicTimelineRelative,
	    topicTimelineJump,
	    topicTimelineTop
	  );
	  const topicTimelineJumpForm = (0, import_html_element.htmlElement)(
	    document,
	    "form",
	    "ldp-topic-timeline-jump-form"
	  );
	  topicTimelineJumpForm.hidden = !0, topicTimelineJumpForm.setAttribute("role", "dialog"), topicTimelineJumpForm.setAttribute("aria-label", "跳到指定楼层");
	  const topicTimelineJumpField = (0, import_html_element.htmlElement)(
	    document,
	    "label",
	    "ldp-topic-timeline-jump-field"
	  ), topicTimelineJumpPrefix = (0, import_html_element.htmlElement)(document, "span", "");
	  topicTimelineJumpPrefix.textContent = "#", topicTimelineJumpPrefix.setAttribute("aria-hidden", "true");
	  const topicTimelineJumpInput = (0, import_html_element.htmlElement)(
	    document,
	    "input",
	    "ldp-topic-timeline-jump-input"
	  );
	  topicTimelineJumpInput.type = "text", topicTimelineJumpInput.inputMode = "numeric", topicTimelineJumpInput.maxLength = 2, topicTimelineJumpInput.autocomplete = "off", topicTimelineJumpInput.spellcheck = !1, topicTimelineJumpInput.setAttribute("enterkeyhint", "go"), topicTimelineJumpInput.setAttribute("aria-label", "楼层号"), topicTimelineJumpInput.setAttribute("aria-invalid", "false");
	  const topicTimelineJumpHint = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-topic-timeline-jump-hint"
	  );
	  topicTimelineJumpHint.id = "ldp-topic-timeline-jump-hint", topicTimelineJumpHint.setAttribute("role", "status"), topicTimelineJumpHint.setAttribute("aria-live", "polite"), topicTimelineJumpHint.setAttribute("aria-atomic", "true"), topicTimelineJumpInput.setAttribute(
	    "aria-describedby",
	    topicTimelineJumpHint.id
	  ), topicTimelineJumpField.append(
	    topicTimelineJumpPrefix,
	    topicTimelineJumpInput
	  );
	  const topicTimelineJumpSubmit = button(
	    options,
	    "ldp-topic-timeline-jump-submit",
	    "跳转",
	    "chevron-right"
	  );
	  topicTimelineJumpSubmit.type = "submit", topicTimelineJumpSubmit.disabled = !0, topicTimelineJumpForm.append(
	    topicTimelineJumpField,
	    topicTimelineJumpSubmit,
	    topicTimelineJumpHint
	  ), topicTimeline.append(
	    topicTimelineDate,
	    topicTimelineTrack,
	    topicTimelineFooter,
	    topicTimelineJumpForm
	  ), readerMain.append(rateLimitNotice, body, topicTimeline);
	  const historyBackEdge = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reader-history-edge ldp-reader-history-edge-back"
	  );
	  historyBackEdge.hidden = !0;
	  const historyBackButton = button(
	    options,
	    "ldp-reader-history-nav ldp-reader-history-back",
	    "上一条阅读历史",
	    "chevron-left"
	  );
	  historyBackButton.hidden = !0, historyBackEdge.append(historyBackButton);
	  const historyForwardEdge = (0, import_html_element.htmlElement)(
	    document,
	    "div",
	    "ldp-reader-history-edge ldp-reader-history-edge-forward"
	  );
	  historyForwardEdge.hidden = !0;
	  const historyForwardButton = button(
	    options,
	    "ldp-reader-history-nav ldp-reader-history-forward",
	    "下一条阅读历史",
	    "chevron-right"
	  );
	  historyForwardButton.hidden = !0, historyForwardEdge.append(historyForwardButton);
	  const liveUpdate = (0, import_html_element.htmlElement)(document, "div", "ldp-live-update");
	  liveUpdate.hidden = !0, liveUpdate.setAttribute("aria-live", "polite");
	  const liveUpdateJump = button(
	    options,
	    "ldp-live-update-jump",
	    "查看新回复",
	    "message-square"
	  ), liveUpdateLabel = (0, import_html_element.htmlElement)(document, "span", "");
	  liveUpdateJump.append(liveUpdateLabel);
	  const liveUpdateDismiss = button(
	    options,
	    "ldp-live-update-dismiss",
	    "暂时关闭新消息提示",
	    "x"
	  );
	  liveUpdate.append(liveUpdateJump, liveUpdateDismiss), modal.append(
	    header,
	    historyBackEdge,
	    readerMain,
	    historyForwardEdge,
	    liveUpdate
	  ), root.append(capsule, hostScrollbar, hostTopButton, modal), options.mount.append(root);
	  const view = Object.freeze({
	    root,
	    modal,
	    body,
	    topicHost: body,
	    surfaceHost: root
	  }), workspaceElements = Object.freeze({
	    pageRoot: document.documentElement,
	    overlay: root,
	    modal,
	    header,
	    titleActions,
	    headButtons: headerActions,
	    windowCapsule: capsule,
	    windowLockButton: lockButton,
	    windowPinButton: pinButton,
	    windowPlacementControl: placementControl,
	    windowPlacementStrip: placementStrip,
	    windowPlacementOptions: placementOptions,
	    embedResizeHandle,
	    hostScrollbar,
	    hostScrollbarThumb,
	    hostTopButton
	  });
	  return Object.freeze({
	    view,
	    workspaceElements,
	    titleJump,
	    metaHost: meta,
	    metaStats,
	    metaOwner,
	    metaOwnerValue,
	    onlyOpToggle,
	    onlyOpProgress,
	    onlyOpProgressValue,
	    topicIdentityHost,
	    headerActions,
	    headerActionsToggle,
	    layoutToggle,
	    titleActions,
	    topicEditTrigger,
	    refreshTopic,
	    openNative,
	    closeReader,
	    rateLimitNotice,
	    rateLimitDetail,
	    rateLimitChallenge,
	    notificationsToggle,
	    notificationUnreadBadge,
	    notificationsPopover,
	    notificationModeTabs: Object.freeze(notificationModeTabs),
	    notificationGroupPanels: Object.freeze(notificationGroupPanels),
	    notificationGroupTabs: Object.freeze(notificationGroupTabs),
	    notificationToolbar,
	    notificationUnreadStatus,
	    notificationMarkAll,
	    notificationNewMessage,
	    notificationSearch,
	    notificationSearchClear,
	    notificationCategoryFilter,
	    notificationTagFilter,
	    notificationList,
	    notificationPagePrevious,
	    notificationPageInfo,
	    notificationPageNext,
	    historyBackEdge,
	    historyForwardEdge,
	    historyBackButton,
	    historyForwardButton,
	    historyToggle,
	    historyPopover,
	    historySortToggle,
	    historyMultiButton,
	    historyClearButton,
	    historyDefaultActions,
	    historyBulkActions,
	    historySelectScope,
	    historySelectToggle,
	    historyDeleteSelected,
	    historyDeleteSelectedLabel,
	    historyMultiDone,
	    historySearch,
	    historySearchClear,
	    historyCategoryFilter,
	    historyTagFilter,
	    historyList,
	    historyPagePrevious,
	    historyPageInfo,
	    historyPageNext,
	    bookmarksToggle,
	    bookmarksPopover,
	    bookmarkTabs: Object.freeze(bookmarkTabs),
	    bookmarksDefaultActions,
	    bookmarksMultiButton,
	    bookmarksBulkActions,
	    bookmarksSelectScope,
	    bookmarksSelectToggle,
	    bookmarksDeleteSelected,
	    bookmarksDeleteSelectedLabel,
	    bookmarksMultiDone,
	    bookmarksSearch,
	    bookmarksSearchClear,
	    bookmarkCategoryFilter,
	    bookmarkTagFilter,
	    bookmarkReactionFilters,
	    bookmarksList,
	    bookmarksPagePrevious,
	    bookmarksPageInfo,
	    bookmarksPageNext,
	    topicTimeline,
	    topicTimelineDate,
	    topicTimelineTrack,
	    topicTimelineCursor,
	    topicTimelineCurrent,
	    topicTimelineTotal,
	    topicTimelinePreview,
	    topicTimelineRelative,
	    topicTimelineJump,
	    topicTimelineTop,
	    topicTimelineJumpForm,
	    topicTimelineJumpInput,
	    topicTimelineJumpSubmit,
	    topicTimelineJumpHint,
	    liveUpdate,
	    liveUpdateJump,
	    liveUpdateLabel,
	    liveUpdateDismiss
	  });
	}
}, "8758d79a8f95f3609795ca6fe1b0511cac27d0e242a47fe0a98186c4d0b6cb94");

/* Source: lite/src/shell/reader-shell.ts */
runtime.register("src/shell/reader-shell.js", function(module, exports, require) {
	var reader_shell_exports = {};
	__export(reader_shell_exports, {
	  ReaderShell: () => ReaderShell,
	  ReaderSurfaceManager: () => ReaderSurfaceManager,
	  createReaderShellStage: () => createReaderShellStage
	});
	module.exports = __toCommonJS(reader_shell_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function compatibilityKey(value) {
	  const key = String(value).trim();
	  if (!key) throw new Error("Reader Shell compatibilityKey 不能为空");
	  return key;
	}
	function abortReason(topicId) {
	  return new DOMException(`Topic ${topicId} 打开已被替代`, "AbortError");
	}
	function validateView(view) {
	  for (const [name, element] of Object.entries(view)) {
	    if (!element || element.nodeType !== 1)
	      throw new TypeError(`Reader Shell ${name} 必须是 Element`);
	    if (name !== "root" && element !== view.root && !view.root.contains(element))
	      throw new Error(`Reader Shell ${name} 必须属于 root`);
	  }
	  return Object.freeze({ ...view });
	}
	class ReaderSurfaceManager {
	  parking;
	  #root;
	  #defaultHost;
	  #surfaces = /* @__PURE__ */ new Set();
	  #destroyed = !1;
	  constructor(root, defaultHost, parentScope) {
	    if (defaultHost !== root && !root.contains(defaultHost))
	      throw new Error("surface defaultHost 必须属于 Shell root");
	    this.#root = root, this.#defaultHost = defaultHost, this.parking = root.ownerDocument.createDocumentFragment(), parentScope?.add(() => this.destroy());
	  }
	  get size() {
	    return this.#surfaces.size;
	  }
	  mount(surface, host = this.#defaultHost) {
	    if (this.#assertActive(), this.#assertHost(host), surface === this.#root) throw new Error("Shell root 不能登记为 surface");
	    return this.#surfaces.add(surface), surface.parentNode !== host && host.append(surface), surface;
	  }
	  park(surface) {
	    if (this.#assertActive(), !this.#surfaces.has(surface))
	      throw new Error("未登记 surface 不能进入 Shell parking");
	    return surface.parentNode !== this.parking && this.parking.append(surface), surface;
	  }
	  destroy() {
	    if (!this.#destroyed) {
	      this.#destroyed = !0;
	      for (const surface of this.#surfaces) surface.remove();
	      this.#surfaces.clear(), this.parking.replaceChildren();
	    }
	  }
	  #assertHost(host) {
	    if (host !== this.#root && !this.#root.contains(host))
	      throw new Error("surface host 必须属于当前 Shell root");
	  }
	  #assertActive() {
	    if (this.#destroyed) throw new Error("ReaderSurfaceManager 已销毁");
	  }
	}
	class ReaderShell {
	  changes = new import_signal.Signal();
	  diagnostics = new import_signal.Signal();
	  compatibilityKey;
	  view;
	  scope;
	  surfaces;
	  #state = "idle";
	  #epoch = 0;
	  #active = null;
	  #opening = null;
	  #deactivation = null;
	  constructor(compatibility, view, parentScope) {
	    this.compatibilityKey = compatibilityKey(compatibility), this.view = validateView(view), this.scope = import_lifecycle.LifecycleScope.ownedBy(parentScope), this.surfaces = new ReaderSurfaceManager(
	      this.view.root,
	      this.view.surfaceHost,
	      this.scope
	    ), this.#syncRootVisibility();
	  }
	  get state() {
	    return this.#state;
	  }
	  get activeTopicId() {
	    return this.#active?.topicId ?? null;
	  }
	  get activeValue() {
	    return this.#active?.value ?? null;
	  }
	  canReuse(compatibility) {
	    return this.#state !== "destroyed" && this.compatibilityKey === compatibilityKey(compatibility);
	  }
	  open(topicIdValue, factory) {
	    if (this.#state === "destroyed")
	      return Promise.resolve({
	        status: "failed",
	        topicId: (0, import_identifiers.discourseTopicId)(topicIdValue),
	        cause: new Error("Reader Shell 已销毁")
	      });
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue);
	    if (this.#opening?.topicId === topicId) return this.#opening.promise;
	    if (!this.#opening && this.#active?.topicId === topicId)
	      return Promise.resolve(Object.freeze({
	        status: "reused",
	        topicId,
	        value: this.#active.value
	      }));
	    this.#epoch += 1, this.#cancelOpening();
	    const controller = new AbortController(), scope = this.scope.child();
	    scope.add(() => {
	      controller.signal.aborted || controller.abort(abortReason(topicId));
	    });
	    const opening = {
	      topicId,
	      epoch: this.#epoch,
	      controller,
	      scope,
	      promise: Promise.resolve({ status: "superseded", topicId })
	    };
	    return this.#opening = opening, this.#setState(this.#active ? "switching" : "opening"), opening.promise = this.#runOpen(opening, factory), opening.promise;
	  }
	  async closeTopic() {
	    if (this.#isDestroyed()) return !0;
	    const epoch = ++this.#epoch;
	    this.#cancelOpening(), this.#setState("closed");
	    const closed = await this.#deactivateActive("close");
	    return this.#isDestroyed() ? !0 : (this.#epoch === epoch && this.#setState("closed"), closed);
	  }
	  destroy() {
	    if (this.#state !== "destroyed") {
	      this.#epoch += 1, this.#cancelOpening(), this.#active = null, this.#setState("destroyed");
	      try {
	        this.scope.destroy();
	      } catch (cause) {
	        this.diagnostics.emit(Object.freeze({
	          phase: "shell-cleanup",
	          topicId: null,
	          cause
	        }));
	      } finally {
	        this.view.root.remove();
	      }
	    }
	  }
	  async #runOpen(opening, factory) {
	    if (!await this.#deactivateActive("switch")) {
	      const cause = new Error("当前 Topic prepareClose 失败"), current = this.#isCurrent(opening);
	      return this.#destroyTopicScope(opening.scope, opening.topicId), current ? (this.#opening = null, this.#setState("running"), Object.freeze({ status: "failed", topicId: opening.topicId, cause })) : Object.freeze({ status: "superseded", topicId: opening.topicId });
	    }
	    if (!this.#isCurrent(opening))
	      return this.#destroyTopicScope(opening.scope, opening.topicId), Object.freeze({ status: "superseded", topicId: opening.topicId });
	    try {
	      const result = await factory(Object.freeze({
	        topicId: opening.topicId,
	        scope: opening.scope,
	        signal: opening.controller.signal,
	        mount: (node) => (this.view.topicHost.append(node), opening.scope.add(() => node.parentNode?.removeChild(node)))
	      }));
	      return typeof result.cleanup == "function" && opening.scope.add(result.cleanup), this.#isCurrent(opening) ? (this.#active = Object.freeze({
	        topicId: opening.topicId,
	        value: result.value,
	        scope: opening.scope,
	        ...result.prepareClose ? { prepareClose: result.prepareClose } : {}
	      }), this.#opening = null, this.#setState("running"), Object.freeze({
	        status: "opened",
	        topicId: opening.topicId,
	        value: result.value
	      })) : (this.#destroyTopicScope(opening.scope, opening.topicId), Object.freeze({ status: "superseded", topicId: opening.topicId }));
	    } catch (cause) {
	      const current = this.#isCurrent(opening);
	      return this.#destroyTopicScope(opening.scope, opening.topicId), current ? (this.#opening = null, this.#setState("failed"), this.diagnostics.emit(Object.freeze({
	        phase: "topic-open",
	        topicId: opening.topicId,
	        cause
	      })), Object.freeze({ status: "failed", topicId: opening.topicId, cause })) : Object.freeze({ status: "superseded", topicId: opening.topicId });
	    }
	  }
	  #cancelOpening() {
	    const opening = this.#opening;
	    opening && (this.#opening = null, opening.controller.signal.aborted || opening.controller.abort(abortReason(opening.topicId)), this.#destroyTopicScope(opening.scope, opening.topicId));
	  }
	  #isCurrent(opening) {
	    return this.#state !== "destroyed" && this.#opening === opening && this.#epoch === opening.epoch && !opening.controller.signal.aborted;
	  }
	  #deactivateActive(reason) {
	    if (!this.#active) return Promise.resolve(!0);
	    if (this.#deactivation) {
	      const deactivation2 = this.#deactivation;
	      return reason === "switch" ? deactivation2 : deactivation2.then((closed) => closed || !this.#active ? closed : this.#deactivateActive("close"));
	    }
	    const active = this.#active, deactivation = (async () => {
	      let prepared = !0;
	      try {
	        await active.prepareClose?.(reason);
	      } catch (cause) {
	        if (prepared = !1, this.diagnostics.emit(Object.freeze({
	          phase: "prepare-close",
	          topicId: active.topicId,
	          cause
	        })), reason === "switch") return !1;
	      }
	      return this.#active === active && (this.#active = null), this.#destroyTopicScope(active.scope, active.topicId), prepared;
	    })().finally(() => {
	      this.#deactivation === deactivation && (this.#deactivation = null);
	    });
	    return this.#deactivation = deactivation, this.#deactivation;
	  }
	  #destroyTopicScope(scope, topicId) {
	    try {
	      scope.destroy();
	    } catch (cause) {
	      this.diagnostics.emit(Object.freeze({
	        phase: "topic-cleanup",
	        topicId,
	        cause
	      }));
	    }
	  }
	  #setState(state) {
	    this.#state !== state && (this.#state = state, this.#syncRootVisibility(), this.changes.emit(state));
	  }
	  #syncRootVisibility() {
	    this.view.root.hidden = this.#state === "idle" || this.#state === "closed" || this.#state === "destroyed";
	  }
	  #isDestroyed() {
	    return this.#state === "destroyed";
	  }
	}
	function createReaderShellStage(options) {
	  return Object.freeze({
	    name: String(options.name ?? "reader-shell").trim() || "reader-shell",
	    required: !0,
	    setup: (scope, context) => {
	      const shell = new ReaderShell(
	        options.compatibilityKey(context),
	        options.createView(context),
	        scope
	      );
	      let readyCleanup;
	      try {
	        readyCleanup = options.onReady?.(shell, context) || void 0;
	      } catch (cause) {
	        throw shell.destroy(), cause;
	      }
	      return () => {
	        try {
	          readyCleanup?.();
	        } finally {
	          shell.destroy();
	        }
	      };
	    }
	  });
	}
}, "aa4bb2cf57a3e3ac5ee0cf9690c2dd7c56caf9ae6d71e1f8e454ea58f8836d20");

/* Source: lite/src/shell/reader-shortcut-controller.ts */
runtime.register("src/shell/reader-shortcut-controller.js", function(module, exports, require) {
	var reader_shortcut_controller_exports = {};
	__export(reader_shortcut_controller_exports, {
	  READER_SHORTCUT_GROUPS: () => READER_SHORTCUT_GROUPS,
	  ReaderShortcutController: () => ReaderShortcutController,
	  readerPreferencesShortcutAdapter: () => readerPreferencesShortcutAdapter,
	  readerShortcutBindingFromEvent: () => readerShortcutBindingFromEvent,
	  readerShortcutBindingIssue: () => readerShortcutBindingIssue,
	  readerShortcutBindingLabel: () => readerShortcutBindingLabel
	});
	module.exports = __toCommonJS(reader_shortcut_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_event_target = require("../dom/event-target.js"), import_signal = require("../kernel/signal.js"), import_reader_preferences_schema = require("../state/reader-preferences-schema.js");
	const readerPreferencesShortcutAdapter = Object.freeze({
	  read: (preferences) => preferences.readerShortcutBindings,
	  createPatch: (readerShortcutBindings) => ({
	    readerShortcutBindings
	  })
	}), definitions = Object.freeze({
	  historyBack: Object.freeze({
	    group: "navigation",
	    label: "上一条阅读历史",
	    description: "切换到上一条帖子并保存当前位置。"
	  }),
	  historyForward: Object.freeze({
	    group: "navigation",
	    label: "下一条阅读历史",
	    description: "切换到下一条帖子并恢复阅读位置。"
	  }),
	  topicTop: Object.freeze({
	    group: "navigation",
	    label: "回到帖子开头",
	    description: "跳到主楼。"
	  }),
	  topicBottom: Object.freeze({
	    group: "navigation",
	    label: "跳到帖子末尾",
	    description: "跳到当前帖子最后可见楼层。"
	  }),
	  floorJump: Object.freeze({
	    group: "navigation",
	    label: "跳到指定楼层",
	    description: "打开楼层号输入框。"
	  }),
	  discussionHorizontalScroll: Object.freeze({
	    group: "navigation",
	    label: "完整讨论横向滚动",
	    description: "树状层级超出宽度时,按滚轮方向左右查看。"
	  }),
	  onlyAuthor: Object.freeze({
	    group: "reading",
	    label: "只看楼主",
	    description: "切换只看楼主模式。"
	  }),
	  translate: Object.freeze({
	    group: "reading",
	    label: "切换正文翻译",
	    description: "在原文、双语和译文之间切换。"
	  }),
	  refreshTopic: Object.freeze({
	    group: "reading",
	    label: "刷新当前帖子",
	    description: "清除当前帖子缓存并重新加载。"
	  }),
	  refreshHost: Object.freeze({
	    group: "reading",
	    label: "刷新嵌入原站",
	    description: "嵌入阅读时刷新后方的原站列表。"
	  }),
	  openOriginal: Object.freeze({
	    group: "reading",
	    label: "打开原帖",
	    description: "在新标签页打开原站帖子。"
	  }),
	  settings: Object.freeze({
	    group: "panels",
	    label: "设置",
	    description: "打开设置面板。"
	  }),
	  notifications: Object.freeze({
	    group: "panels",
	    label: "消息",
	    description: "打开或关闭通知与私信面板。"
	  }),
	  historyPanel: Object.freeze({
	    group: "panels",
	    label: "浏览历史面板",
	    description: "打开或关闭浏览历史列表。"
	  }),
	  bookmarksPanel: Object.freeze({
	    group: "panels",
	    label: "收藏与回应面板",
	    description: "打开或关闭收藏与回应列表。"
	  }),
	  likeTopic: Object.freeze({
	    group: "topicActions",
	    label: "点赞主帖",
	    description: "点赞或取消点赞当前主帖。"
	  }),
	  replyTopic: Object.freeze({
	    group: "topicActions",
	    label: "回复主题",
	    description: "打开当前主题的回复编辑器。"
	  }),
	  bookmarkTopic: Object.freeze({
	    group: "topicActions",
	    label: "收藏主题",
	    description: "收藏或编辑当前主题收藏。"
	  }),
	  toggleFullscreen: Object.freeze({
	    group: "window",
	    label: "全屏/浮窗切换",
	    description: "在全屏阅读与浮窗阅读之间切换。"
	  }),
	  toggleQueue: Object.freeze({
	    group: "window",
	    label: "阅读队列",
	    description: "展开或收起阅读队列。"
	  }),
	  closeReader: Object.freeze({
	    group: "window",
	    label: "关闭阅读器",
	    description: "遵循“连续两次关闭”安全设置退出阅读器。"
	  })
	}), groupCopy = Object.freeze({
	  navigation: Object.freeze({
	    title: "浏览导航",
	    description: "在阅读历史和当前帖子的关键位置之间移动。"
	  }),
	  reading: Object.freeze({
	    title: "阅读工具",
	    description: "切换阅读过滤、翻译和内容刷新。"
	  }),
	  panels: Object.freeze({
	    title: "界面面板",
	    description: "快速打开阅读器里的常用信息面板。"
	  }),
	  topicActions: Object.freeze({
	    title: "帖子操作",
	    description: "复用主帖操作列,不复制点赞、回复或收藏逻辑。"
	  }),
	  window: Object.freeze({
	    title: "窗口与队列",
	    description: "控制阅读器窗口、阅读队列与退出。"
	  })
	}), READER_SHORTCUT_GROUPS = Object.freeze(
	  Object.keys(groupCopy).map(
	    (group) => Object.freeze({
	      id: group,
	      ...groupCopy[group],
	      actions: Object.freeze(import_reader_preferences_schema.READER_SHORTCUT_ACTIONS.filter((id) => definitions[id].group === group).map((id) => Object.freeze({
	        id,
	        label: definitions[id].label,
	        description: definitions[id].description
	      })))
	    })
	  )
	);
	function readerShortcutBindingLabel(binding) {
	  const labels = Object.freeze({
	    Ctrl: "Ctrl",
	    Alt: "Alt",
	    Shift: "Shift",
	    Meta: "Meta",
	    ArrowLeft: "←",
	    ArrowRight: "→",
	    ArrowUp: "↑",
	    ArrowDown: "↓",
	    Home: "Home",
	    End: "End",
	    PageUp: "Page Up",
	    PageDown: "Page Down",
	    Space: "空格",
	    Escape: "Esc",
	    Enter: "Enter",
	    Tab: "Tab",
	    Comma: ",",
	    Period: ".",
	    Slash: "/",
	    Semicolon: ";",
	    Quote: "'",
	    BracketLeft: "[",
	    BracketRight: "]",
	    Backslash: "\\",
	    Minus: "-",
	    Equal: "=",
	    Backquote: "`",
	    Mouse1: "鼠标中键",
	    Mouse3: "鼠标后退键",
	    Mouse4: "鼠标前进键",
	    Wheel: "滚轮"
	  });
	  return binding.split("+").map(
	    (part) => labels[part] ?? (/^Key[A-Z]$/.test(part) ? part.slice(3) : /^Digit\d$/.test(part) ? part.slice(5) : /^Numpad\d$/.test(part) ? `数字键盘 ${part.slice(6)}` : /^Mouse\d+$/.test(part) ? `鼠标键 ${Number(part.slice(5)) + 1}` : part)
	  ).join(" + ");
	}
	function readerShortcutBindingFromEvent(event) {
	  const source = event, wheel = event.type === "wheel", mouse = /^(?:mouse|auxclick)/.test(event.type);
	  if (wheel && !source.ctrlKey && !source.altKey && !source.shiftKey && !source.metaKey) return "";
	  const code = wheel ? "Wheel" : mouse ? source.button === 1 || Number(source.button) >= 3 ? `Mouse${source.button}` : "" : String(source.code ?? "");
	  return !code || !mouse && !wheel && /^(?:Control|Alt|Shift|Meta)(?:Left|Right)?$/.test(code) ? "" : (0, import_reader_preferences_schema.normalizeReaderShortcutBinding)([
	    source.ctrlKey && "Ctrl",
	    source.altKey && "Alt",
	    source.shiftKey && "Shift",
	    source.metaKey && "Meta",
	    code
	  ].filter(Boolean).join("+"));
	}
	function readerShortcutBindingIssue(bindings, binding, exceptAction) {
	  const normalized = (0, import_reader_preferences_schema.normalizeReaderShortcutBinding)(binding);
	  if (!normalized) return "这个按键组合无法识别,请换一个。";
	  const owner = import_reader_preferences_schema.READER_SHORTCUT_ACTIONS.find(
	    (action) => action !== exceptAction && bindings[action].includes(normalized)
	  );
	  if (owner)
	    return `${readerShortcutBindingLabel(normalized)} 已绑定“${definitions[owner].label}”,请先移除或改用其他组合。`;
	  const policyIssue = (0, import_reader_preferences_schema.readerShortcutBindingPolicyIssue)(normalized);
	  return policyIssue === "reserved" ? `${readerShortcutBindingLabel(normalized)} 通常由浏览器占用,无法保证生效,请换一个组合。` : policyIssue === "bare-alphanumeric" ? "单个字母或数字容易与论坛快捷键冲突,请至少加入 Ctrl、Alt、Shift 或 Meta。" : "";
	}
	function editableTarget(event) {
	  const target = (0, import_event_target.eventElement)(event);
	  if (!target) return !1;
	  const element = target;
	  return element.closest(
	    'input,textarea,select,[contenteditable="true"],[contenteditable=""]'
	  ) ? !0 : event.type !== "keydown" ? !1 : !!element.closest(
	    'button,a[href],[role="button"],[role="slider"],[role="tab"],[role="menuitem"],[role="dialog"]'
	  );
	}
	class ReaderShortcutController {
	  scope;
	  changes = new import_signal.Signal();
	  captures = new import_signal.Signal();
	  #adapter;
	  #persist;
	  #execute;
	  #canExecute;
	  #onUnavailable;
	  #onError;
	  #bindings;
	  #recording = null;
	  #mouseGuard = null;
	  constructor(options) {
	    this.#adapter = options.preferences, this.#persist = options.persist, this.#execute = options.execute, this.#canExecute = options.canExecute ?? (() => !0), this.#onUnavailable = options.onUnavailable ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.#bindings = (0, import_reader_preferences_schema.normalizeReaderShortcutBindings)(
	      this.#adapter.read(options.readPreferences())
	    ), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), options.preferenceChanges.subscribe((preferences) => {
	      this.#bindings = (0, import_reader_preferences_schema.normalizeReaderShortcutBindings)(
	        this.#adapter.read(preferences)
	      ), this.#publish();
	    }, this.scope), this.scope.listen(options.target, "keydown", (event) => {
	      this.#handle(event);
	    }, !1), this.scope.listen(options.target, "wheel", (event) => {
	      this.#handle(event);
	    }, { capture: !0, passive: !1 }), this.scope.listen(options.target, "mousedown", (event) => {
	      this.#handle(event);
	    }, !0), this.scope.listen(options.target, "mouseup", (event) => {
	      this.#handle(event);
	    }, !0), this.scope.listen(options.target, "auxclick", (event) => {
	      this.#handle(event);
	    }, !0), this.scope.add(() => {
	      this.changes.clear(), this.captures.clear();
	    });
	  }
	  get snapshot() {
	    return Object.freeze({
	      bindings: this.#bindings,
	      recording: this.#recording
	    });
	  }
	  startRecording(action) {
	    if (!import_reader_preferences_schema.READER_SHORTCUT_ACTIONS.includes(action))
	      throw new RangeError(`未知快捷动作:${action}`);
	    this.#recording = this.#recording === action ? null : action, this.#publish();
	  }
	  cancelRecording() {
	    this.#recording !== null && (this.#recording = null, this.#publish());
	  }
	  remove(action, binding) {
	    this.#replace(action, this.#bindings[action].filter(
	      (candidate) => candidate !== binding
	    ));
	  }
	  clear(action) {
	    this.#replace(action, Object.freeze([]));
	  }
	  reset(action) {
	    const defaults = import_reader_preferences_schema.READER_SHORTCUT_DEFAULTS[action], issue = defaults.map((binding) => readerShortcutBindingIssue(this.#bindings, binding, action)).find(Boolean);
	    return issue || (this.#replace(action, defaults), "");
	  }
	  resetAll() {
	    this.#write((0, import_reader_preferences_schema.normalizeReaderShortcutBindings)(import_reader_preferences_schema.READER_SHORTCUT_DEFAULTS));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #handle(event) {
	    const keyboard = event, mouse = event;
	    if (event.type !== "mousedown" && this.#mouseGuard && mouse.button === this.#mouseGuard.button && Date.now() < this.#mouseGuard.until) {
	      event.preventDefault(), event.stopImmediatePropagation();
	      return;
	    }
	    if (event.defaultPrevented || keyboard.isComposing || keyboard.repeat) return;
	    const binding = readerShortcutBindingFromEvent(event);
	    if (!binding) return;
	    if (this.#recording) {
	      this.#consume(event, mouse);
	      const action2 = this.#recording;
	      if (this.#bindings[action2].includes(binding)) {
	        this.captures.emit(Object.freeze({
	          action: action2,
	          binding,
	          accepted: !1,
	          message: `${readerShortcutBindingLabel(binding)} 已在该动作中。`
	        }));
	        return;
	      }
	      const issue = readerShortcutBindingIssue(
	        this.#bindings,
	        binding,
	        action2
	      );
	      if (!issue && this.#bindings[action2].length >= 3) {
	        this.captures.emit(Object.freeze({
	          action: action2,
	          binding,
	          accepted: !1,
	          message: "每项最多保留 3 个快捷方式。"
	        }));
	        return;
	      }
	      if (issue) {
	        this.captures.emit(Object.freeze({
	          action: action2,
	          binding,
	          accepted: !1,
	          message: issue
	        }));
	        return;
	      }
	      this.#replace(action2, Object.freeze([
	        ...this.#bindings[action2],
	        binding
	      ])), this.#recording = null, this.captures.emit(Object.freeze({
	        action: action2,
	        binding,
	        accepted: !0,
	        message: `已绑定 ${readerShortcutBindingLabel(binding)}`
	      })), this.#publish();
	      return;
	    }
	    const targetBlocked = editableTarget(event), action = import_reader_preferences_schema.READER_SHORTCUT_ACTIONS.find(
	      (candidate) => this.#bindings[candidate].includes(binding)
	    );
	    if (action) {
	      if (targetBlocked || !this.#canExecute(action, event)) {
	        event.type !== "keydown" && this.#consume(event, mouse);
	        return;
	      }
	      try {
	        const result = this.#execute(action, event);
	        if (result === !1 && action === "discussionHorizontalScroll")
	          return;
	        if (this.#consume(event, mouse), result === !1) {
	          this.#onUnavailable(action, definitions[action].label);
	          return;
	        }
	        result && typeof result.then == "function" && Promise.resolve(result).then((value) => {
	          value === !1 && this.#onUnavailable(action, definitions[action].label);
	        }).catch(this.#onError);
	      } catch (cause) {
	        this.#onError(cause);
	      }
	    }
	  }
	  #consume(event, mouse) {
	    event.preventDefault(), event.stopImmediatePropagation(), event.type === "mousedown" && (this.#mouseGuard = Object.freeze({
	      button: Number(mouse.button),
	      until: Date.now() + 800
	    }));
	  }
	  #replace(action, bindings) {
	    this.#write((0, import_reader_preferences_schema.normalizeReaderShortcutBindings)({
	      ...this.#bindings,
	      [action]: bindings
	    }));
	  }
	  #write(bindings) {
	    const persisted = this.#persist(this.#adapter.createPatch(bindings));
	    this.#bindings = (0, import_reader_preferences_schema.normalizeReaderShortcutBindings)(
	      this.#adapter.read(persisted)
	    ), this.#publish();
	  }
	  #publish() {
	    this.scope.destroyed || this.changes.emit(this.snapshot);
	  }
	}
}, "56c75eeed42c96ccea449ca0f6c743739d6da190d504b4d0429202d9e20ef593");

/* Source: lite/src/shell/reader-surface-portal.ts */
runtime.register("src/shell/reader-surface-portal.js", function(module, exports, require) {
	var reader_surface_portal_exports = {};
	__export(reader_surface_portal_exports, {
	  ReaderSurfacePortal: () => ReaderSurfacePortal
	});
	module.exports = __toCommonJS(reader_surface_portal_exports);
	const PORTAL_ID = "ldp-mian-lite-portal";
	class ReaderSurfacePortal {
	  host;
	  root;
	  style;
	  #destroyed = !1;
	  constructor(document, stylesheet) {
	    if (!document.documentElement)
	      throw new Error("Reader Surface Portal 缺少 documentElement");
	    if (document.getElementById(PORTAL_ID))
	      throw new Error("Reader Surface Portal 已存在");
	    const host = document.createElement("div");
	    host.id = PORTAL_ID, host.className = "ldp-reader-portal-host sciapp-ldp-owned", host.dataset.ldpReaderPortal = "mian-lite";
	    const root = host.attachShadow({ mode: "open" }), style = document.createElement("style");
	    style.dataset.ldpReaderShadow = "mian-lite", style.textContent = stylesheet, root.append(style), document.documentElement.append(host), this.host = host, this.root = root, this.style = style;
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.host.remove());
	  }
	}
}, "fa2ac6ce26a1fa3db363281f1f16bf64a67328c388b03d2ee73e2b0fa3ccc621");

/* Source: lite/src/shell/reader-topic-edit-form-surface.ts */
runtime.register("src/shell/reader-topic-edit-form-surface.js", function(module, exports, require) {
	var reader_topic_edit_form_surface_exports = {};
	__export(reader_topic_edit_form_surface_exports, {
	  ReaderTopicEditFormSurface: () => ReaderTopicEditFormSurface
	});
	module.exports = __toCommonJS(reader_topic_edit_form_surface_exports);
	var import_html_element = require("../dom/html-element.js"), import_reader_action_form_support = require("./reader-action-form-support.js");
	function tagKey(value) {
	  return String(value ?? "").trim().toLocaleLowerCase();
	}
	function categorySearchText(category, byId) {
	  return `${(category.parentCategoryId ? byId.get(category.parentCategoryId) : null)?.name ?? ""} ${category.name}`.trim().toLocaleLowerCase();
	}
	class ReaderTopicEditFormSurface {
	  scope;
	  #host;
	  constructor(options) {
	    this.#host = new import_reader_action_form_support.ReaderActionFormSurfaceHost({
	      ...options,
	      label: "ReaderTopicEditFormSurface"
	    }), this.scope = this.#host.scope;
	  }
	  get #document() {
	    return this.#host.document;
	  }
	  open(request) {
	    if (request.signal?.aborted) return Promise.resolve(!1);
	    const categories = request.categories.filter((category) => Number.isSafeInteger(category.id) && category.id > 0 && String(category.name).trim());
	    if (!categories.length)
	      return Promise.reject(new Error("原站分类数据尚未就绪,请稍后重试"));
	    const categoryById = new Map(categories.map((category) => [
	      category.id,
	      category
	    ]));
	    let selectedCategoryId = categoryById.has(request.categoryId) ? request.categoryId : categories[0].id;
	    const availableTags = /* @__PURE__ */ new Map(), selectedTagKeys = /* @__PURE__ */ new Set();
	    for (const tag of request.tags) {
	      const key = tagKey(tag.name);
	      key && (availableTags.set(key, tag), selectedTagKeys.add(key));
	    }
	    const { id, previousFocus } = this.#host.prepare(), layer = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-reader-action-layer ldp-topic-edit-layer"
	    ), dialog = (0, import_html_element.htmlElement)(
	      this.#document,
	      "section",
	      "ldp-reader-action-dialog ldp-topic-edit-dialog"
	    );
	    dialog.setAttribute("role", "dialog"), dialog.setAttribute("aria-modal", "true");
	    const titleId = `ldp-topic-edit-dialog-title-${id}`;
	    dialog.setAttribute("aria-labelledby", titleId);
	    const head = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-reader-action-head"), heading = (0, import_html_element.htmlElement)(this.#document, "strong", "", "编辑帖子信息");
	    heading.id = titleId;
	    const close = this.#iconButton(
	      "ldp-reader-action-close ldp-topic-edit-close",
	      "关闭编辑弹层",
	      "x"
	    );
	    close.dataset.topicEditClose = "", head.append(heading, close);
	    const form = (0, import_html_element.htmlElement)(
	      this.#document,
	      "form",
	      "ldp-reader-action-form ldp-topic-edit-form"
	    ), body = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-reader-action-body ldp-topic-edit-body"
	    ), titleField = (0, import_html_element.htmlElement)(
	      this.#document,
	      "label",
	      "ldp-topic-edit-field ldp-topic-edit-title"
	    ), titleLabel = (0, import_html_element.htmlElement)(this.#document, "span", "", "标题"), titleInput = this.#document.createElement("input");
	    titleInput.name = "title", titleInput.type = "text", titleInput.maxLength = 255, titleInput.required = !0, titleInput.value = String(request.title ?? "").trim(), titleField.append(titleLabel, titleInput);
	    const categoryField = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-topic-edit-field"
	    ), categoryLabel = (0, import_html_element.htmlElement)(this.#document, "span", "", "类别");
	    categoryLabel.id = `ldp-topic-edit-category-label-${id}`;
	    const categoryRoot = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-topic-edit-category"
	    ), categoryTrigger = (0, import_html_element.htmlElement)(
	      this.#document,
	      "button",
	      "ldp-topic-edit-category-trigger ldp-picker-trigger"
	    );
	    categoryTrigger.type = "button", categoryTrigger.setAttribute("aria-haspopup", "listbox"), categoryTrigger.setAttribute("aria-expanded", "false"), categoryTrigger.setAttribute("aria-labelledby", categoryLabel.id);
	    const categoryValue = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-topic-edit-category-value"
	    );
	    categoryTrigger.append(categoryValue, this.#icon("chevron-right"));
	    const categoryMenu = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-topic-edit-category-menu"
	    );
	    categoryMenu.hidden = !0;
	    const categorySearchLabel = (0, import_html_element.htmlElement)(
	      this.#document,
	      "label",
	      "ldp-topic-edit-category-search ldp-picker-search"
	    );
	    categorySearchLabel.append(this.#icon("search"));
	    const categorySearch = this.#document.createElement("input");
	    categorySearch.name = "category-search", categorySearch.type = "search", categorySearch.autocomplete = "off", categorySearch.placeholder = "搜索类别", categorySearch.setAttribute("aria-label", "搜索类别"), categorySearchLabel.append(categorySearch);
	    const categoryOptions = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-topic-edit-category-options ldp-picker-options"
	    );
	    categoryOptions.setAttribute("role", "listbox"), categoryOptions.setAttribute("aria-labelledby", categoryLabel.id), categoryMenu.append(categorySearchLabel, categoryOptions), categoryRoot.append(categoryTrigger, categoryMenu), categoryField.append(categoryLabel, categoryRoot);
	    const tagField = (0, import_html_element.htmlElement)(this.#document, "label", "ldp-topic-edit-field"), tagLabel = (0, import_html_element.htmlElement)(this.#document, "span", "", "label"), tagControl = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-topic-edit-label-control"
	    ), tagValues = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-topic-edit-label-values"
	    ), tagInput = (0, import_html_element.htmlElement)(
	      this.#document,
	      "input",
	      "ldp-topic-edit-label-input"
	    );
	    tagInput.name = "tag-search", tagInput.type = "text", tagInput.autocomplete = "off", tagInput.placeholder = "搜索 label", tagInput.setAttribute("aria-label", "添加 label");
	    const tagOptionsId = `ldp-topic-edit-label-options-${id}`;
	    tagInput.setAttribute("list", tagOptionsId);
	    const tagOptions = (0, import_html_element.htmlElement)(
	      this.#document,
	      "datalist",
	      "ldp-topic-edit-label-options"
	    );
	    tagOptions.id = tagOptionsId, tagControl.append(tagValues, tagInput), tagField.append(tagLabel, tagControl, tagOptions);
	    const status = (0, import_html_element.htmlElement)(this.#document, "p", "ldp-topic-edit-status");
	    status.setAttribute("role", "status"), status.setAttribute("aria-live", "polite"), body.append(titleField, categoryField, tagField, status);
	    const footer = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-reader-action-footer ldp-topic-edit-actions"
	    ), submit = this.#iconButton(
	      "ldp-topic-edit-save",
	      "保存帖子信息",
	      "check"
	    );
	    submit.type = "submit";
	    const cancel = this.#iconButton(
	      "ldp-topic-edit-cancel",
	      "取消编辑",
	      "x"
	    );
	    cancel.dataset.topicEditCancel = "", footer.append(submit, cancel), form.append(body, footer), dialog.append(head, form), layer.append(dialog);
	    {
	      let busy = !1, searchTimer = null, searchSequence = 0;
	      const selectedTags = () => Object.freeze([...selectedTagKeys].map((key) => availableTags.get(key)).filter((tag) => !!tag)), clearSearch = () => {
	        searchSequence += 1, searchTimer !== null && clearTimeout(searchTimer), searchTimer = null;
	      }, frame = {
	        layer,
	        dialog,
	        form,
	        body,
	        status,
	        cancel,
	        submit
	      }, session = this.#host.start({
	        frame,
	        previousFocus,
	        closeSelector: "[data-topic-edit-close]",
	        cancelSelector: "[data-topic-edit-cancel]",
	        signal: request.signal,
	        onSettled: clearSearch
	      }), closeCategoryMenu = (restoreFocus = !1) => {
	        categoryMenu.hidden = !0, categoryTrigger.setAttribute("aria-expanded", "false"), restoreFocus && !categoryTrigger.disabled && categoryTrigger.focus({ preventScroll: !0 });
	      }, setBusy = (value) => {
	        busy = value, session.setBusy(value), titleInput.disabled = value, categoryTrigger.disabled = value, categorySearch.disabled = value, tagInput.disabled = value, submit.disabled = value, cancel.disabled = value, close.disabled = value, form.setAttribute("aria-busy", String(value)), value && closeCategoryMenu();
	      }, syncCategory = () => {
	        const selected = categoryById.get(selectedCategoryId), parent = selected?.parentCategoryId ? categoryById.get(selected.parentCategoryId) : null;
	        categoryValue.textContent = selected ? `${parent ? `${parent.name} / ` : ""}${selected.name}` : "请选择类别";
	        for (const option of categoryOptions.querySelectorAll(
	          ".ldp-topic-edit-category-option"
	        ))
	          option.setAttribute(
	            "aria-selected",
	            String(Number(option.dataset.categoryId) === selectedCategoryId)
	          );
	      }, renderCategories = () => {
	        const fragment = this.#document.createDocumentFragment();
	        for (const category of categories) {
	          const parent = category.parentCategoryId ? categoryById.get(category.parentCategoryId) : null, option = (0, import_html_element.htmlElement)(
	            this.#document,
	            "button",
	            "ldp-topic-edit-category-option ldp-picker-option"
	          );
	          option.type = "button", option.dataset.categoryId = String(category.id), option.dataset.searchText = categorySearchText(
	            category,
	            categoryById
	          ), option.setAttribute("role", "option");
	          const dot = (0, import_html_element.htmlElement)(
	            this.#document,
	            "span",
	            "ldp-topic-edit-category-dot"
	          );
	          /^[0-9a-f]{6}$/i.test(category.color) && dot.style.setProperty(
	            "--ldp-category-color",
	            `#${category.color}`
	          );
	          const copy = (0, import_html_element.htmlElement)(
	            this.#document,
	            "span",
	            "ldp-topic-edit-category-copy ldp-picker-option-copy"
	          ), name = (0, import_html_element.htmlElement)(
	            this.#document,
	            "strong",
	            "",
	            category.name
	          ), level = (0, import_html_element.htmlElement)(
	            this.#document,
	            "small",
	            "",
	            parent?.name ?? "一级类别"
	          );
	          copy.append(name, level), option.append(dot, copy), fragment.append(option);
	        }
	        categoryOptions.replaceChildren(fragment), syncCategory();
	      }, scheduleTagSearch = () => {
	        clearSearch();
	        const sequence = searchSequence;
	        searchTimer = setTimeout(() => {
	          searchTimer = null, request.searchTags({
	            query: tagInput.value.trim(),
	            categoryId: selectedCategoryId,
	            selected: selectedTags()
	          }).then((tags) => {
	            if (!(!session.active || sequence !== searchSequence)) {
	              for (const tag of tags) {
	                const key = tagKey(tag.name);
	                key && availableTags.set(key, tag);
	              }
	              tagOptions.replaceChildren(...tags.map((tag) => {
	                const option = (0, import_html_element.htmlElement)(this.#document, "option");
	                return option.value = tag.name, option;
	              })), status.textContent = "";
	            }
	          }).catch((cause) => {
	            !session.active || sequence !== searchSequence || (status.textContent = cause instanceof Error ? cause.message : "标签搜索失败,请重试");
	          });
	        }, 300);
	      }, renderTags = () => {
	        tagValues.replaceChildren(...selectedTags().map((tag) => {
	          const chip = (0, import_html_element.htmlElement)(
	            this.#document,
	            "span",
	            "ldp-topic-edit-label-chip"
	          ), copy = (0, import_html_element.htmlElement)(this.#document, "span", "", tag.name), remove = this.#iconButton(
	            "ldp-topic-edit-label-remove",
	            `移除 label ${tag.name}`,
	            "x"
	          );
	          return remove.dataset.labelKey = tagKey(tag.name), chip.append(copy, remove), chip;
	        }));
	      }, commitTagInput = (showError = !0) => {
	        const raw = tagInput.value.replace(/[,,]\s*$/, "").trim();
	        if (!raw)
	          return tagInput.value = "", !0;
	        const key = tagKey(raw);
	        return availableTags.get(key) ? (selectedTagKeys.add(key), tagInput.value = "", status.textContent = "", renderTags(), scheduleTagSearch(), !0) : (showError && (status.textContent = `没有找到 label“${raw}”`), !1);
	      };
	      return layer.addEventListener("click", (event) => {
	        const target = event.target;
	        target && (target.closest(".ldp-topic-edit-category") || closeCategoryMenu());
	      }), layer.addEventListener("keydown", (event) => {
	        const keyboard = event;
	        if (keyboard.key === "Escape" && !categoryMenu.hidden && !busy) {
	          keyboard.preventDefault(), keyboard.stopImmediatePropagation(), closeCategoryMenu(!0);
	          return;
	        }
	        keyboard.key === "Escape" && !busy && keyboard.stopPropagation();
	      }), categoryTrigger.addEventListener("click", () => {
	        if (!busy) {
	          if (!categoryMenu.hidden) {
	            closeCategoryMenu(!0);
	            return;
	          }
	          categoryMenu.hidden = !1, categoryTrigger.setAttribute("aria-expanded", "true"), categorySearch.value = "";
	          for (const option of categoryOptions.children)
	            option.hidden = !1;
	          categorySearch.focus({ preventScroll: !0 });
	        }
	      }), categoryTrigger.addEventListener("keydown", (event) => {
	        event.key === "ArrowDown" && (event.preventDefault(), categoryTrigger.click());
	      }), categorySearch.addEventListener("input", () => {
	        const query = categorySearch.value.trim().toLocaleLowerCase();
	        for (const option of categoryOptions.querySelectorAll(
	          ".ldp-topic-edit-category-option"
	        ))
	          option.hidden = !!(query && !String(option.dataset.searchText).includes(query));
	      }), categoryOptions.addEventListener("click", (event) => {
	        const option = event.target?.closest(
	          ".ldp-topic-edit-category-option"
	        );
	        !option || busy || (selectedCategoryId = Number(option.dataset.categoryId), syncCategory(), closeCategoryMenu(!0), scheduleTagSearch());
	      }), tagInput.addEventListener("input", scheduleTagSearch), tagInput.addEventListener("change", () => commitTagInput(!1)), tagInput.addEventListener("keydown", (event) => {
	        const key = event.key;
	        ["Enter", ",", ","].includes(key) && (event.preventDefault(), commitTagInput());
	      }), tagValues.addEventListener("click", (event) => {
	        const remove = event.target?.closest(
	          ".ldp-topic-edit-label-remove"
	        );
	        !remove || busy || (selectedTagKeys.delete(String(remove.dataset.labelKey)), renderTags(), scheduleTagSearch(), tagInput.focus({ preventScroll: !0 }));
	      }), form.addEventListener("submit", (event) => {
	        if (event.preventDefault(), busy || !commitTagInput()) return;
	        const nextTitle = titleInput.value.trim(), category = categoryById.get(selectedCategoryId);
	        if (!nextTitle) {
	          status.textContent = "标题不能为空", titleInput.focus({ preventScroll: !0 });
	          return;
	        }
	        if (!category) {
	          status.textContent = "请选择类别", categoryTrigger.focus({ preventScroll: !0 });
	          return;
	        }
	        setBusy(!0), status.textContent = "正在保存…", new Promise((resolve) => {
	          resolve(request.submit(Object.freeze({
	            title: nextTitle,
	            category,
	            tags: selectedTags()
	          })));
	        }).then(() => {
	          session.active && session.settle(!0);
	        }).catch((cause) => {
	          session.active && (status.textContent = cause instanceof Error ? cause.message : "保存失败,请重试", setBusy(!1));
	        });
	      }), renderCategories(), renderTags(), session.mount(() => {
	        titleInput.focus({ preventScroll: !0 }), titleInput.select();
	      }), scheduleTagSearch(), session.result;
	    }
	  }
	  destroy() {
	    this.#host.destroy();
	  }
	  #icon(name) {
	    return (0, import_reader_action_form_support.renderReaderActionIcon)(
	      this.#document,
	      name,
	      this.#host.renderIcon
	    );
	  }
	  #iconButton(className, label, icon) {
	    const button = (0, import_html_element.htmlElement)(this.#document, "button", className);
	    return button.type = "button", button.setAttribute("aria-label", label), button.append(this.#icon(icon)), button;
	  }
	}
}, "964d8ccccee41bb4adf4de61bc618d25ece0686372c1447f235cf320c47e7195");

/* Source: lite/src/shell/reader-workspace-coordinator.ts */
runtime.register("src/shell/reader-workspace-coordinator.js", function(module, exports, require) {
	var reader_workspace_coordinator_exports = {};
	__export(reader_workspace_coordinator_exports, {
	  ReaderHeaderAlignmentController: () => ReaderHeaderAlignmentController,
	  ReaderHostTakeoverController: () => ReaderHostTakeoverController,
	  ReaderWorkspaceCoordinator: () => ReaderWorkspaceCoordinator,
	  createReaderShellWorkspaceStage: () => createReaderShellWorkspaceStage
	});
	module.exports = __toCommonJS(reader_workspace_coordinator_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_embedded_host_appearance = require("./embedded-host-appearance.js"), import_embedded_host_root_controller = require("./embedded-host-root-controller.js"), import_embedded_host_scrollbar = require("./embedded-host-scrollbar.js"), import_embedded_host_top_shortcut = require("./embedded-host-top-shortcut.js"), import_main_outlet_mutation_hub = require("./main-outlet-mutation-hub.js"), import_reader_embed_resize_controller = require("./reader-embed-resize-controller.js"), import_reader_shell = require("./reader-shell.js"), import_reader_workspace = require("./reader-workspace.js");
	const READER_VISIBLE_STATES = /* @__PURE__ */ new Set([
	  "opening",
	  "switching",
	  "running",
	  "failed"
	]);
	class ReaderHostTakeoverController {
	  scope;
	  #shell;
	  #workspace;
	  #pageRoot;
	  #routeKind;
	  #destroyed = !1;
	  constructor(options) {
	    this.#shell = options.shell, this.#workspace = options.workspace, this.#pageRoot = options.pageRoot, this.#routeKind = options.routeKind, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#shell.changes.subscribe(() => this.#sync(), this.scope), this.#workspace.changes.subscribe(() => this.#sync(), this.scope), this.scope.add(() => this.#clear()), this.#sync();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #sync() {
	    if (this.#destroyed) return;
	    const visible = READER_VISIBLE_STATES.has(this.#shell.state), embedded = this.#workspace.snapshot.presentation.embedded;
	    this.#pageRoot.classList.toggle("ldp-reader-open", visible), this.#pageRoot.classList.toggle(
	      "ldp-scroll-lock",
	      visible && !embedded
	    ), this.#pageRoot.classList.toggle(
	      "ldp-route-takeover",
	      visible && this.#routeKind === "direct-topic"
	    );
	  }
	  #clear() {
	    this.#pageRoot.classList.remove(
	      "ldp-reader-open",
	      "ldp-scroll-lock",
	      "ldp-route-takeover"
	    );
	  }
	}
	class ReaderHeaderAlignmentController {
	  scope;
	  #shell;
	  #workspace;
	  #elements;
	  #requestFrame;
	  #cancelFrame;
	  #resizeObserver;
	  #mutationObserver;
	  #observedSizes = /* @__PURE__ */ new WeakMap();
	  #content = null;
	  #frame = 0;
	  #titleFrame = 0;
	  #titleDirty = !0;
	  #lastAlignment = null;
	  #destroyed = !1;
	  constructor(options) {
	    this.#shell = options.shell, this.#workspace = options.workspace, this.#elements = options.elements, this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#resizeObserver = options.createResizeObserver?.(
	      (entries) => this.#onResize(entries)
	    ) ?? null, this.#resizeObserver?.observe(this.#elements.modal), this.#resizeObserver?.observe(this.#elements.header), this.#mutationObserver = options.createMutationObserver?.((records) => {
	      records.some(
	        (record) => record.target === this.#shell.view.root || this.#elements.header.contains(record.target)
	      ) && this.#schedule(!0);
	    }) ?? null, this.#mutationObserver?.observe(this.#shell.view.root, {
	      attributes: !0,
	      attributeFilter: ["style"]
	    }), this.#mutationObserver?.observe(this.#elements.header, {
	      attributes: !0,
	      attributeFilter: ["hidden"],
	      childList: !0,
	      characterData: !0,
	      subtree: !0
	    }), this.#shell.changes.subscribe(() => this.#schedule(!0), this.scope), this.#workspace.changes.subscribe(() => this.#schedule(!0), this.scope), this.scope.add(() => {
	      this.#resizeObserver?.disconnect(), this.#mutationObserver?.disconnect(), this.#frame && this.#cancelFrame(this.#frame), this.#titleFrame && this.#cancelFrame(this.#titleFrame), this.#clear();
	    }), this.#schedule(!0);
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #schedule(titleDirty = !0) {
	    titleDirty && (this.#titleDirty = !0), !(this.#destroyed || this.#frame) && (this.#frame = this.#requestFrame(() => {
	      this.#frame = 0, this.#sync();
	    }));
	  }
	  #onResize(entries) {
	    if (!entries.length) {
	      this.#schedule(!1);
	      return;
	    }
	    let sizeChanged = !1;
	    for (const entry of entries) {
	      const borderSize = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize, width = borderSize?.inlineSize ?? entry.contentRect.width, height = borderSize?.blockSize ?? entry.contentRect.height;
	      if (!Number.isFinite(width) || width < 0 || !Number.isFinite(height) || height < 0) continue;
	      const size = entry.target === this.#elements.modal || entry.target === this.#elements.header ? `${Math.round(width * 2)}:${Math.round(height * 2)}` : `${Math.round(width * 2)}`, previous = this.#observedSizes.get(entry.target);
	      this.#observedSizes.set(entry.target, size), previous !== size && (entry.target === this.#elements.header && (this.#titleDirty = !0), sizeChanged = !0);
	    }
	    sizeChanged && this.#schedule(!1);
	  }
	  #sync() {
	    if (this.#destroyed) return;
	    this.#elements.modal.classList.toggle(
	      "ldp-reader-surface-short",
	      this.#elements.modal.clientHeight <= 560
	    );
	    const content = this.#elements.overlay.querySelector(".ldp-topic-runtime") ?? Array.from(
	      this.#elements.overlay.querySelectorAll(".ldp-comments")
	    ).find((candidate) => candidate.getBoundingClientRect().width > 0) ?? null;
	    if (content !== this.#content && (this.#resizeObserver?.disconnect(), this.#resizeObserver?.observe(this.#elements.modal), this.#resizeObserver?.observe(this.#elements.header), content && this.#resizeObserver?.observe(content), this.#content = content, this.#lastAlignment = null, this.#titleDirty = !0), !this.#workspace.snapshot.presentation.fullPage || !content) {
	      this.#clearGeometry(), this.#scheduleTitleActions();
	      return;
	    }
	    const modalRect = this.#elements.modal.getBoundingClientRect(), contentRect = content.getBoundingClientRect();
	    if (!(modalRect.width > 0) || !(contentRect.width > 0)) return;
	    const left = Math.max(0, Math.round(contentRect.left - modalRect.left)), right = Math.max(0, Math.round(modalRect.right - contentRect.right)), alignment = `${left}:${right}`;
	    if (alignment !== this.#lastAlignment) {
	      this.#lastAlignment = alignment, this.#titleDirty = !0;
	      const header = this.#elements.header;
	      header.style.setProperty("--ldp-header-logo-inset", `${left}px`), header.style.paddingLeft = `${left}px`, header.style.paddingRight = `${right}px`, this.#elements.titleActions.style.right = `${right}px`, this.#elements.headButtons.classList.add("is-content-aligned");
	    }
	    this.#scheduleTitleActions();
	  }
	  #scheduleTitleActions() {
	    this.#destroyed || !this.#titleDirty || this.#titleFrame || (this.#titleFrame = this.#requestFrame(() => {
	      this.#titleFrame = 0, !(this.#destroyed || !this.#titleDirty) && (this.#titleDirty = !1, this.#syncTitleActions());
	    }));
	  }
	  #syncTitleActions() {
	    const { header, titleActions } = this.#elements, titleJump = header.querySelector(".ldp-title-jump");
	    if (!titleJump) return;
	    const titleRange = titleActions.ownerDocument.createRange();
	    titleRange.selectNodeContents(titleJump);
	    const titleRects = Array.from(titleRange.getClientRects()).filter(
	      (rect) => rect.width > 0 && rect.height > 0
	    ), firstTop = titleRects[0]?.top, singleLine = firstTop !== void 0 && titleRects.every((rect) => Math.abs(rect.top - firstTop) < 1), singleLineChanged = header.classList.contains("ldp-title-single-line") !== singleLine, view = titleActions.ownerDocument.defaultView, actionIcon = Array.from(titleActions.children).find(
	      (control) => control instanceof HTMLElement && !control.hidden && view?.getComputedStyle(control).display !== "none"
	    )?.querySelector(".ldp-icon");
	    if (!actionIcon) {
	      header.classList.toggle("ldp-title-single-line", singleLine), titleActions.style.setProperty("--ldp-title-actions-align-y", "0px"), singleLineChanged && (this.#titleDirty = !0, this.#scheduleTitleActions());
	      return;
	    }
	    const titleTextRect = titleRects[0], graphicTop = Array.from(actionIcon.querySelectorAll(
	      ":is(path,circle,ellipse,line,polyline,polygon,rect,use)"
	    )).reduce((top, graphic) => {
	      const rect = graphic.getBoundingClientRect();
	      return rect.width > 0 && rect.height > 0 ? Math.min(top, rect.top) : top;
	    }, 1 / 0), titleTop = titleTextRect?.top ?? titleJump.getBoundingClientRect().top, actionTop = Number.isFinite(graphicTop) ? graphicTop : actionIcon.getBoundingClientRect().top, shift = (Number.parseFloat(
	      titleActions.style.getPropertyValue("--ldp-title-actions-align-y")
	    ) || 0) + titleTop - actionTop;
	    header.classList.toggle("ldp-title-single-line", singleLine), titleActions.style.setProperty(
	      "--ldp-title-actions-align-y",
	      `${Math.round(shift * 2) / 2}px`
	    ), singleLineChanged && (this.#titleDirty = !0, this.#scheduleTitleActions());
	  }
	  #clearGeometry() {
	    if (this.#lastAlignment === null) return;
	    this.#lastAlignment = null, this.#titleDirty = !0;
	    const { header, titleActions, headButtons } = this.#elements;
	    header.style.removeProperty("--ldp-header-logo-inset"), header.style.removeProperty("padding-left"), header.style.removeProperty("padding-right"), titleActions.style.removeProperty("right"), headButtons.classList.remove("is-content-aligned");
	  }
	  #clear() {
	    this.#clearGeometry(), this.#elements.modal.classList.remove("ldp-reader-surface-short"), this.#elements.titleActions.style.removeProperty(
	      "--ldp-title-actions-align-y"
	    );
	  }
	}
	class ReaderWorkspaceCoordinator {
	  scope;
	  workspace;
	  window;
	  mutations;
	  #onPersistMode;
	  #onPersistWindow;
	  #destroyed = !1;
	  constructor(options) {
	    const viewport = options.readViewport(), requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id));
	    this.#onPersistMode = options.onPersistMode, this.#onPersistWindow = options.onPersistWindow, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    try {
	      this.workspace = new import_reader_workspace.ReaderWorkspaceModel({
	        routeKind: options.routeKind,
	        requestedMode: options.requestedMode,
	        embedWidth: options.embedWidth,
	        viewportWidth: viewport.width,
	        ...options.active === void 0 ? {} : { active: options.active }
	      }), this.window = new import_reader_workspace.ReaderWindowGeometryModel({
	        preferences: options.windowPreferences,
	        viewportWidth: viewport.width,
	        viewportHeight: viewport.height,
	        mode: this.workspace.snapshot.presentation.mode
	      }), this.workspace.changes.subscribe((snapshot) => {
	        this.window.setMode(snapshot.presentation.mode);
	      }, this.scope), new import_reader_workspace.ReaderWorkspaceDomAdapter({
	        model: this.workspace,
	        pageRoot: options.elements.pageRoot,
	        overlay: options.elements.overlay,
	        parentScope: this.scope
	      }), options.elements.windowCapsule && options.elements.windowPlacementControl && options.elements.windowPlacementStrip && options.elements.windowPlacementOptions && new import_reader_workspace.ReaderWorkspacePlacementController({
	        model: this.workspace,
	        routeKind: options.routeKind,
	        capsule: options.elements.windowCapsule,
	        control: options.elements.windowPlacementControl,
	        strip: options.elements.windowPlacementStrip,
	        options: options.elements.windowPlacementOptions,
	        onSelect: (mode) => this.setMode(mode),
	        parentScope: this.scope
	      }), new import_reader_workspace.ReaderWindowDomAdapter({
	        model: this.window,
	        overlay: options.elements.overlay,
	        modal: options.elements.modal,
	        header: options.elements.header,
	        ...options.elements.windowLockButton ? { lockButton: options.elements.windowLockButton } : {},
	        ...options.elements.windowPinButton ? { pinButton: options.elements.windowPinButton } : {},
	        parentScope: this.scope
	      }), new import_reader_workspace.ReaderWindowPointerController({
	        model: this.window,
	        overlay: options.elements.overlay,
	        modal: options.elements.modal,
	        header: options.elements.header,
	        ...options.elements.windowLockButton ? { lockButton: options.elements.windowLockButton } : {},
	        ...options.elements.windowPinButton ? { pinButton: options.elements.windowPinButton } : {},
	        viewportTarget: options.viewportTarget,
	        readViewport: options.readViewport,
	        ...options.onPersistWindow ? { onPersist: options.onPersistWindow } : {},
	        requestFrame,
	        cancelFrame,
	        parentScope: this.scope
	      }), new import_reader_embed_resize_controller.ReaderEmbedResizeController({
	        model: this.workspace,
	        pageRoot: options.elements.pageRoot,
	        overlay: options.elements.overlay,
	        handle: options.elements.embedResizeHandle,
	        viewportTarget: options.viewportTarget,
	        readViewportWidth: () => options.readViewport().width,
	        ...options.onPersistEmbedWidth ? { onPersist: options.onPersistEmbedWidth } : {},
	        requestFrame,
	        cancelFrame,
	        parentScope: this.scope
	      }), this.mutations = new import_main_outlet_mutation_hub.MainOutletMutationHub({
	        document: options.document,
	        ...options.createMutationObserver ? { createObserver: options.createMutationObserver } : {}
	      }), this.scope.add(() => this.mutations.destroy()), new import_embedded_host_root_controller.EmbeddedHostRootController({
	        model: this.workspace,
	        routeKind: options.routeKind,
	        document: options.document,
	        overlay: options.elements.overlay,
	        mutations: this.mutations,
	        enhancements: options.enhancements,
	        ...options.topicFilterChanges ? { topicFilterChanges: options.topicFilterChanges } : {},
	        requestFrame,
	        cancelFrame,
	        parentScope: this.scope
	      }), new import_embedded_host_scrollbar.EmbeddedHostScrollbarController({
	        workspace: this.workspace,
	        track: options.elements.hostScrollbar,
	        thumb: options.elements.hostScrollbarThumb,
	        scrollTarget: options.scrollTarget,
	        scroll: options.hostScroll,
	        resizeTargets: Object.freeze([
	          options.elements.pageRoot,
	          ...options.document.body ? [options.document.body] : []
	        ]),
	        ...options.createResizeObserver ? { createResizeObserver: options.createResizeObserver } : {},
	        ...options.readHostScrollbarTrack ? { readTrack: options.readHostScrollbarTrack } : {},
	        requestFrame,
	        cancelFrame,
	        parentScope: this.scope
	      }), new import_embedded_host_top_shortcut.EmbeddedHostTopShortcutController({
	        workspace: this.workspace,
	        button: options.elements.hostTopButton,
	        pointerTarget: options.pointerTarget,
	        scrollTarget: options.scrollTarget,
	        readScrollTop: () => options.hostScroll.readScrollTop?.() ?? options.hostScroll.read().scrollTop,
	        readViewportHeight: () => options.readViewport().height,
	        scrollToTop: () => options.hostScroll.scrollTo(0),
	        requestFrame,
	        cancelFrame,
	        parentScope: this.scope
	      }), new import_embedded_host_appearance.EmbeddedHostAppearanceController({
	        workspace: this.workspace,
	        pageRoot: options.elements.pageRoot,
	        overlay: options.elements.overlay,
	        readAppearance: options.readAppearance,
	        ...options.appearanceChanges ? { appearanceChanges: options.appearanceChanges } : {},
	        ...options.measureHostRowHeight ? { measureRowHeight: options.measureHostRowHeight } : {},
	        parentScope: this.scope
	      });
	    } catch (error) {
	      throw this.scope.destroy(), error;
	    }
	  }
	  setMode(mode) {
	    const accepted = this.workspace.setRequestedMode(mode);
	    return accepted && this.#onPersistMode?.(mode), accepted;
	  }
	  setEmbedWidth(width) {
	    return this.workspace.setEmbedWidth(width);
	  }
	  setWindowGeometry(width, height, left, top) {
	    const previous = this.window.snapshot;
	    this.window.setGeometry(width, height, left, top) !== previous && this.#persistWindow();
	  }
	  setWindowLocked(locked) {
	    const previous = this.window.snapshot;
	    this.window.setLocked(locked) !== previous && this.#persistWindow();
	  }
	  setWindowPinned(pinned) {
	    const previous = this.window.snapshot;
	    this.window.setPinned(pinned) !== previous && this.#persistWindow();
	  }
	  resetWindow() {
	    const previous = this.window.snapshot;
	    this.window.reset() !== previous && this.#persistWindow();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #persistWindow() {
	    this.#onPersistWindow?.(this.window.preferencePatch());
	  }
	}
	function createReaderShellWorkspaceStage(options) {
	  return (0, import_reader_shell.createReaderShellStage)({
	    name: options.name ?? "reader-shell-workspace",
	    compatibilityKey: options.compatibilityKey,
	    createView: options.createView,
	    onReady(shell, context) {
	      const workspaceOptions = options.createWorkspaceOptions(shell, context), workspace = new ReaderWorkspaceCoordinator({
	        ...workspaceOptions,
	        active: READER_VISIBLE_STATES.has(shell.state),
	        parentScope: shell.scope
	      });
	      shell.changes.subscribe((state) => {
	        workspace.workspace.setActive(READER_VISIBLE_STATES.has(state));
	      }, workspace.scope);
	      const takeover = new ReaderHostTakeoverController({
	        shell,
	        workspace: workspace.workspace,
	        pageRoot: workspaceOptions.elements.pageRoot,
	        routeKind: workspaceOptions.routeKind,
	        parentScope: shell.scope
	      }), headerAlignment = new ReaderHeaderAlignmentController({
	        shell,
	        workspace: workspace.workspace,
	        elements: workspaceOptions.elements,
	        ...workspaceOptions.createMutationObserver ? {
	          createMutationObserver: workspaceOptions.createMutationObserver
	        } : {},
	        ...workspaceOptions.createResizeObserver ? {
	          createResizeObserver: workspaceOptions.createResizeObserver
	        } : {},
	        ...workspaceOptions.requestFrame ? { requestFrame: workspaceOptions.requestFrame } : {},
	        ...workspaceOptions.cancelFrame ? { cancelFrame: workspaceOptions.cancelFrame } : {},
	        parentScope: shell.scope
	      });
	      let readyCleanup;
	      try {
	        readyCleanup = options.onReady?.(shell, workspace, context) || void 0;
	      } catch (error) {
	        throw headerAlignment.destroy(), takeover.destroy(), workspace.destroy(), error;
	      }
	      return () => {
	        try {
	          readyCleanup?.();
	        } finally {
	          try {
	            headerAlignment.destroy();
	          } finally {
	            try {
	              takeover.destroy();
	            } finally {
	              workspace.destroy();
	            }
	          }
	        }
	      };
	    }
	  });
	}
}, "b9f01618d579ebc56f8448623af7c76dd1325da88760aff35e13193d5d0b8157");

/* Source: lite/src/shell/reader-workspace.ts */
runtime.register("src/shell/reader-workspace.js", function(module, exports, require) {
	var reader_workspace_exports = {};
	__export(reader_workspace_exports, {
	  READER_COMPACT_MAX_WIDTH: () => READER_COMPACT_MAX_WIDTH,
	  READER_EMBED_MIN_WIDTH: () => READER_EMBED_MIN_WIDTH,
	  READER_HOST_MIN_WIDTH: () => READER_HOST_MIN_WIDTH,
	  READER_WINDOW_MARGIN: () => READER_WINDOW_MARGIN,
	  READER_WINDOW_MIN_HEIGHT: () => READER_WINDOW_MIN_HEIGHT,
	  READER_WINDOW_MIN_WIDTH: () => READER_WINDOW_MIN_WIDTH,
	  ReaderWindowDomAdapter: () => ReaderWindowDomAdapter,
	  ReaderWindowGeometryModel: () => ReaderWindowGeometryModel,
	  ReaderWindowPointerController: () => ReaderWindowPointerController,
	  ReaderWorkspaceDomAdapter: () => ReaderWorkspaceDomAdapter,
	  ReaderWorkspaceModel: () => ReaderWorkspaceModel,
	  ReaderWorkspacePlacementController: () => ReaderWorkspacePlacementController,
	  readerWorkspacePositionMode: () => readerWorkspacePositionMode
	});
	module.exports = __toCommonJS(reader_workspace_exports);
	var import_event_target = require("../dom/event-target.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function readerWorkspacePositionMode(mode) {
	  return mode === "fullpage" ? "fullpage" : mode === "embed-left" || mode === "embed-right" ? "embedded" : "floating";
	}
	const READER_EMBED_MIN_WIDTH = 360, READER_HOST_MIN_WIDTH = 680, READER_WINDOW_MARGIN = 8, READER_WINDOW_MIN_WIDTH = 360, READER_WINDOW_MIN_HEIGHT = 320, READER_COMPACT_MAX_WIDTH = 700, WINDOW_DEFAULT_WIDTH = 1080, WINDOW_DEFAULT_VIEWPORT_WIDTH = 0.94, WINDOW_DEFAULT_VIEWPORT_HEIGHT = 0.86, WINDOW_HANDLE_DOCK_THRESHOLD_PX = 2, DEFAULT_WINDOW_GEOMETRY_POLICY = Object.freeze({
	  margin: READER_WINDOW_MARGIN,
	  minWidth: READER_WINDOW_MIN_WIDTH,
	  minHeight: READER_WINDOW_MIN_HEIGHT,
	  compactWidth: READER_COMPACT_MAX_WIDTH,
	  defaultWidth: WINDOW_DEFAULT_WIDTH,
	  defaultHeight: null,
	  defaultViewportWidth: WINDOW_DEFAULT_VIEWPORT_WIDTH,
	  defaultViewportHeight: WINDOW_DEFAULT_VIEWPORT_HEIGHT
	});
	function projectReaderWindowPlacement(overlay, geometry) {
	  if (overlay.classList.toggle(
	    "ldp-window-handle-docked",
	    !!(geometry && geometry.top <= WINDOW_HANDLE_DOCK_THRESHOLD_PX)
	  ), !geometry) {
	    overlay.style.removeProperty("--ldp-reader-window-center-x"), overlay.style.removeProperty("--ldp-reader-window-top");
	    return;
	  }
	  overlay.style.setProperty(
	    "--ldp-reader-window-center-x",
	    `${geometry.left + geometry.width / 2}px`
	  ), overlay.style.setProperty("--ldp-reader-window-top", `${geometry.top}px`);
	}
	const PRESENTATIONS = Object.freeze({
	  floating: Object.freeze({
	    mode: "floating",
	    floating: !0,
	    fullPage: !1,
	    embedded: !1,
	    side: ""
	  }),
	  fullpage: Object.freeze({
	    mode: "fullpage",
	    floating: !1,
	    fullPage: !0,
	    embedded: !1,
	    side: ""
	  }),
	  "embed-left": Object.freeze({
	    mode: "embed-left",
	    floating: !1,
	    fullPage: !1,
	    embedded: !0,
	    side: "left"
	  }),
	  "embed-right": Object.freeze({
	    mode: "embed-right",
	    floating: !1,
	    fullPage: !1,
	    embedded: !0,
	    side: "right"
	  })
	}), LIST_MODES = /* @__PURE__ */ new Set([
	  "floating",
	  "fullpage",
	  "embed-left",
	  "embed-right"
	]), DIRECT_TOPIC_MODES = /* @__PURE__ */ new Set(["floating", "fullpage"]);
	function finiteViewport(value, name) {
	  if (!Number.isFinite(value) || value < 1)
	    throw new RangeError(`${name} 必须是正有限数`);
	  return value;
	}
	function finiteNumber(value, fallback) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? numeric : fallback;
	}
	function windowGeometryPolicy(value) {
	  const source = {
	    ...DEFAULT_WINDOW_GEOMETRY_POLICY,
	    ...value
	  }, positive = (candidate, name) => {
	    const numeric = Number(candidate);
	    if (!Number.isFinite(numeric) || numeric <= 0)
	      throw new RangeError(`${name} 必须是正有限数`);
	    return numeric;
	  }, compactWidth = Number(source.compactWidth);
	  if (!Number.isFinite(compactWidth) || compactWidth < 0)
	    throw new RangeError("compactWidth 必须是非负有限数");
	  const defaultHeight = source.defaultHeight === null ? null : positive(source.defaultHeight, "defaultHeight");
	  return Object.freeze({
	    margin: positive(source.margin, "margin"),
	    minWidth: positive(source.minWidth, "minWidth"),
	    minHeight: positive(source.minHeight, "minHeight"),
	    compactWidth,
	    defaultWidth: positive(source.defaultWidth, "defaultWidth"),
	    defaultHeight,
	    defaultViewportWidth: positive(
	      source.defaultViewportWidth,
	      "defaultViewportWidth"
	    ),
	    defaultViewportHeight: positive(
	      source.defaultViewportHeight,
	      "defaultViewportHeight"
	    )
	  });
	}
	function workspacePresentation(mode) {
	  return PRESENTATIONS[mode];
	}
	function workspaceModeAllowed(mode, routeKind) {
	  return (routeKind === "direct-topic" ? DIRECT_TOPIC_MODES : LIST_MODES).has(mode);
	}
	function dispatchCompatibilityEvent(target, name) {
	  const event = target.ownerDocument.createEvent("Event");
	  event.initEvent(name, !1, !1), target.dispatchEvent(event);
	}
	class ReaderWorkspaceModel {
	  changes = new import_signal.Signal();
	  #routeKind;
	  #requestedMode;
	  #viewportWidth;
	  #requestedEmbedWidth;
	  #active;
	  #snapshot;
	  constructor(options) {
	    this.#routeKind = options.routeKind, this.#viewportWidth = finiteViewport(options.viewportWidth, "viewportWidth"), this.#active = options.active !== !1, this.#requestedMode = workspaceModeAllowed(options.requestedMode, this.#routeKind) ? options.requestedMode : this.#fallbackMode(), this.#requestedEmbedWidth = Math.max(
	      READER_EMBED_MIN_WIDTH,
	      Math.round(finiteNumber(options.embedWidth, READER_EMBED_MIN_WIDTH))
	    ), this.#snapshot = this.#derive();
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  setRequestedMode(mode) {
	    return workspaceModeAllowed(mode, this.#routeKind) ? (this.#requestedMode = mode, this.#commit(), !0) : !1;
	  }
	  /**
	   * Shell 关闭时只撤销 presentation,requestedMode 仍作为下次打开的偏好。
	   */
	  setActive(active) {
	    return this.#active = active, this.#commit(), this.#snapshot;
	  }
	  setEmbedWidth(width) {
	    return this.#requestedEmbedWidth = this.#clampEmbedWidth(width), this.#commit(), this.#snapshot.embedWidth;
	  }
	  resizeViewport(width) {
	    return this.#viewportWidth = finiteViewport(width, "viewportWidth"), this.#commit(), this.#snapshot;
	  }
	  #fallbackMode() {
	    return this.#routeKind === "direct-topic" ? "fullpage" : "floating";
	  }
	  #canEmbed() {
	    return this.#viewportWidth >= READER_EMBED_MIN_WIDTH + READER_HOST_MIN_WIDTH;
	  }
	  #maximumEmbedWidth() {
	    return Math.max(
	      READER_EMBED_MIN_WIDTH,
	      Math.floor(this.#viewportWidth - READER_HOST_MIN_WIDTH)
	    );
	  }
	  #clampEmbedWidth(value) {
	    return Math.min(
	      this.#maximumEmbedWidth(),
	      Math.max(
	        READER_EMBED_MIN_WIDTH,
	        Math.round(finiteNumber(value, READER_EMBED_MIN_WIDTH))
	      )
	    );
	  }
	  #derive() {
	    const requested = workspaceModeAllowed(this.#requestedMode, this.#routeKind) ? this.#requestedMode : this.#fallbackMode(), effective = this.#active ? workspacePresentation(requested).embedded && !this.#canEmbed() ? "floating" : requested : this.#fallbackMode();
	    return Object.freeze({
	      requestedMode: requested,
	      presentation: workspacePresentation(effective),
	      viewportWidth: this.#viewportWidth,
	      canEmbed: this.#canEmbed(),
	      embedWidth: this.#clampEmbedWidth(this.#requestedEmbedWidth)
	    });
	  }
	  #commit() {
	    const next = this.#derive();
	    next.requestedMode === this.#snapshot.requestedMode && next.presentation.mode === this.#snapshot.presentation.mode && next.viewportWidth === this.#snapshot.viewportWidth && next.embedWidth === this.#snapshot.embedWidth || (this.#snapshot = next, this.changes.emit(next));
	  }
	}
	class ReaderWorkspaceDomAdapter {
	  scope;
	  #model;
	  #pageRoot;
	  #overlay;
	  #dispatchEvents;
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#pageRoot = options.pageRoot, this.#overlay = options.overlay, this.#dispatchEvents = options.dispatchEvents !== !1, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#model.changes.subscribe((snapshot) => this.#apply(snapshot, !0), this.scope), this.scope.add(() => this.#clear()), this.#apply(this.#model.snapshot, !1);
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #apply(snapshot, changed) {
	    if (this.#destroyed) return;
	    const presentation = snapshot.presentation;
	    if (this.#overlay.dataset.readerWorkspaceMode = presentation.mode, this.#overlay.classList.toggle("ldp-fullpage", presentation.fullPage), this.#overlay.classList.toggle("ldp-reader-embedded", presentation.embedded), this.#overlay.classList.toggle(
	      "ldp-reader-embedded-left",
	      presentation.side === "left"
	    ), this.#overlay.classList.toggle(
	      "ldp-reader-embedded-right",
	      presentation.side === "right"
	    ), this.#pageRoot.classList.toggle("ldp-reader-workspace", presentation.embedded), this.#pageRoot.classList.toggle(
	      "ldp-reader-embedded-left",
	      presentation.side === "left"
	    ), this.#pageRoot.classList.toggle(
	      "ldp-reader-embedded-right",
	      presentation.side === "right"
	    ), presentation.embedded) {
	      const width = `${snapshot.embedWidth}px`;
	      this.#overlay.dataset.readerWorkspaceWidth = String(snapshot.embedWidth), this.#overlay.style.setProperty("--ldp-reader-workspace-width", width), this.#pageRoot.style.setProperty("--ldp-reader-workspace-width", width);
	    } else {
	      delete this.#overlay.dataset.readerWorkspaceWidth;
	      for (const target of [this.#overlay, this.#pageRoot])
	        target.style.removeProperty("--ldp-reader-workspace-width");
	    }
	    changed && this.#dispatchEvents && dispatchCompatibilityEvent(this.#overlay, "ldp-reader-workspace-change");
	  }
	  #clear() {
	    for (const className of [
	      "ldp-fullpage",
	      "ldp-reader-embedded",
	      "ldp-reader-embedded-left",
	      "ldp-reader-embedded-right"
	    ])
	      this.#overlay.classList.remove(className);
	    this.#pageRoot.classList.remove(
	      "ldp-reader-workspace",
	      "ldp-reader-embedded-left",
	      "ldp-reader-embedded-right"
	    ), delete this.#overlay.dataset.readerWorkspaceMode, delete this.#overlay.dataset.readerWorkspaceWidth;
	    for (const target of [this.#overlay, this.#pageRoot])
	      target.style.removeProperty("--ldp-reader-workspace-width");
	  }
	}
	class ReaderWorkspacePlacementController {
	  scope;
	  #model;
	  #capsule;
	  #control;
	  #strip;
	  #options;
	  #allowedModes;
	  #onSelect;
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#capsule = options.capsule, this.#control = options.control, this.#strip = options.strip, this.#options = options.options, this.#allowedModes = options.routeKind === "direct-topic" ? DIRECT_TOPIC_MODES : LIST_MODES, this.#onSelect = options.onSelect, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    for (const option of this.#options)
	      this.scope.listen(option, "click", (event) => {
	        event.preventDefault(), event.stopPropagation();
	        const mode = option.dataset.readerPlacement;
	        !mode || !this.#allowedModes.has(mode) || this.#onSelect(mode);
	      });
	    this.#model.changes.subscribe(() => this.#sync(), this.scope), this.scope.add(() => this.#clear()), this.#sync();
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #sync() {
	    if (this.#destroyed) return;
	    const available = this.#allowedModes.size > 1;
	    this.#control.hidden = !available, this.#strip.hidden = !available, this.#capsule.classList.toggle("ldp-reader-placement-available", available);
	    const activeMode = this.#model.snapshot.presentation.mode;
	    for (const option of this.#options) {
	      const mode = option.dataset.readerPlacement, allowed = !!(mode && this.#allowedModes.has(mode)), active = allowed && mode === activeMode;
	      option.hidden = !allowed, option.disabled = !allowed, option.classList.toggle("active", active), option.setAttribute("aria-pressed", String(active));
	    }
	  }
	  #clear() {
	    this.#capsule.classList.remove("ldp-reader-placement-available"), this.#control.hidden = !0, this.#strip.hidden = !0;
	    for (const option of this.#options)
	      option.classList.remove("active"), option.removeAttribute("aria-pressed");
	  }
	}
	class ReaderWindowGeometryModel {
	  changes = new import_signal.Signal();
	  #policy;
	  #viewportWidth;
	  #viewportHeight;
	  #presentation;
	  #geometry;
	  #geometryPersisted;
	  #locked;
	  #pinned;
	  #snapshot;
	  constructor(options) {
	    this.#policy = windowGeometryPolicy(options.policy), this.#viewportWidth = finiteViewport(options.viewportWidth, "viewportWidth"), this.#viewportHeight = finiteViewport(options.viewportHeight, "viewportHeight"), this.#presentation = workspacePresentation(options.mode), this.#locked = options.preferences.readerWindowLocked, this.#pinned = options.preferences.readerWindowPinned, this.#geometryPersisted = !!(options.preferences.readerWindowWidth || options.preferences.readerWindowHeight || options.preferences.readerWindowX || options.preferences.readerWindowY), this.#geometry = this.#preferredGeometry(options.preferences), this.#snapshot = this.#derive();
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  setMode(mode) {
	    return this.#presentation = workspacePresentation(mode), this.#commit(), this.#snapshot;
	  }
	  resizeViewport(width, height) {
	    return this.#viewportWidth = finiteViewport(width, "viewportWidth"), this.#viewportHeight = finiteViewport(height, "viewportHeight"), this.#geometryPersisted || (this.#geometry = this.#centeredGeometry()), this.#commit(), this.#snapshot;
	  }
	  /** 内容 owner 可按可见组件的固有宽度更新缩放下限。 */
	  setMinimumWidth(width) {
	    const minWidth = finiteViewport(width, "minWidth");
	    return minWidth === this.#policy.minWidth ? this.#snapshot : (this.#policy = Object.freeze({ ...this.#policy, minWidth }), this.#geometry = this.#clampGeometry(this.#geometry), this.#commit(), this.#snapshot);
	  }
	  setGeometry(width, height, left, top) {
	    if (this.#viewportWidth <= this.#policy.compactWidth) return this.#snapshot;
	    const current = this.#geometry;
	    return this.#geometry = this.#clampGeometry({
	      width: finiteNumber(width, current.width),
	      height: finiteNumber(height, current.height),
	      left: finiteNumber(left, current.left),
	      top: finiteNumber(top, current.top)
	    }), this.#geometryPersisted = !0, this.#commit(), this.#snapshot;
	  }
	  setLocked(locked) {
	    return this.#viewportWidth <= this.#policy.compactWidth ? this.#snapshot : (this.#locked = !!locked, this.#commit(), this.#snapshot);
	  }
	  setPinned(pinned) {
	    return this.#viewportWidth <= this.#policy.compactWidth ? this.#snapshot : (this.#pinned = !!pinned, this.#commit(), this.#snapshot);
	  }
	  reset() {
	    return this.#viewportWidth <= this.#policy.compactWidth ? this.#snapshot : (this.#locked = !1, this.#pinned = !1, this.#geometryPersisted = !1, this.#geometry = this.#centeredGeometry(), this.#commit(), this.#snapshot);
	  }
	  preferencePatch() {
	    return Object.freeze({
	      readerWindowWidth: this.#geometryPersisted ? Math.round(this.#geometry.width) : 0,
	      readerWindowHeight: this.#geometryPersisted ? Math.round(this.#geometry.height) : 0,
	      readerWindowX: this.#geometryPersisted ? Math.round(this.#geometry.left) : 0,
	      readerWindowY: this.#geometryPersisted ? Math.round(this.#geometry.top) : 0,
	      readerWindowLocked: this.#locked,
	      readerWindowPinned: this.#pinned
	    });
	  }
	  previewGeometry(width, height, left, top) {
	    return this.#clampGeometry({ width, height, left, top });
	  }
	  previewResize(start, direction, deltaX, deltaY) {
	    let left = start.left, top = start.top, right = start.left + start.width, bottom = start.top + start.height;
	    const bounds = this.#viewportBounds(), minWidth = Math.min(this.#policy.minWidth, bounds.maxWidth), minHeight = Math.min(this.#policy.minHeight, bounds.maxHeight);
	    return direction.includes("w") && (left = Math.min(
	      right - minWidth,
	      Math.max(this.#policy.margin, start.left + deltaX)
	    )), direction.includes("e") && (right = Math.max(
	      left + minWidth,
	      Math.min(
	        this.#viewportWidth - this.#policy.margin,
	        right + deltaX
	      )
	    )), direction.includes("n") && (top = Math.min(
	      bottom - minHeight,
	      Math.max(this.#policy.margin, start.top + deltaY)
	    )), direction.includes("s") && (bottom = Math.max(
	      top + minHeight,
	      Math.min(
	        this.#viewportHeight - this.#policy.margin,
	        bottom + deltaY
	      )
	    )), this.#clampGeometry({
	      left,
	      top,
	      width: right - left,
	      height: bottom - top
	    });
	  }
	  #managed() {
	    return this.#presentation.floating && this.#viewportWidth > this.#policy.compactWidth;
	  }
	  #viewportBounds() {
	    return {
	      maxWidth: Math.max(
	        1,
	        this.#viewportWidth - this.#policy.margin * 2
	      ),
	      maxHeight: Math.max(
	        1,
	        this.#viewportHeight - this.#policy.margin * 2
	      )
	    };
	  }
	  #clampSize(width, height) {
	    const bounds = this.#viewportBounds();
	    return {
	      width: Math.min(
	        bounds.maxWidth,
	        Math.max(
	          Math.min(this.#policy.minWidth, bounds.maxWidth),
	          Math.round(width)
	        )
	      ),
	      height: Math.min(
	        bounds.maxHeight,
	        Math.max(
	          Math.min(this.#policy.minHeight, bounds.maxHeight),
	          Math.round(height)
	        )
	      )
	    };
	  }
	  #centeredGeometry() {
	    const size = this.#clampSize(
	      Math.min(
	        this.#policy.defaultWidth,
	        this.#viewportWidth * this.#policy.defaultViewportWidth
	      ),
	      Math.min(
	        this.#policy.defaultHeight ?? Number.POSITIVE_INFINITY,
	        this.#viewportHeight * this.#policy.defaultViewportHeight
	      )
	    );
	    return this.#clampGeometry({
	      ...size,
	      left: Math.round((this.#viewportWidth - size.width) / 2),
	      top: Math.round((this.#viewportHeight - size.height) / 2)
	    });
	  }
	  #preferredGeometry(preferences) {
	    const fallback = this.#centeredGeometry(), size = this.#clampSize(
	      preferences.readerWindowWidth || fallback.width,
	      preferences.readerWindowHeight || fallback.height
	    );
	    return this.#clampGeometry({
	      ...size,
	      left: preferences.readerWindowX || Math.round(
	        (this.#viewportWidth - size.width) / 2
	      ),
	      top: preferences.readerWindowY || Math.round(
	        (this.#viewportHeight - size.height) / 2
	      )
	    });
	  }
	  #clampGeometry(value) {
	    const size = this.#clampSize(value.width, value.height);
	    return Object.freeze({
	      ...size,
	      left: Math.min(
	        this.#viewportWidth - this.#policy.margin - size.width,
	        Math.max(this.#policy.margin, Math.round(value.left))
	      ),
	      top: Math.min(
	        this.#viewportHeight - this.#policy.margin - size.height,
	        Math.max(this.#policy.margin, Math.round(value.top))
	      )
	    });
	  }
	  #derive() {
	    return Object.freeze({
	      geometry: this.#clampGeometry(this.#geometry),
	      viewportWidth: this.#viewportWidth,
	      viewportHeight: this.#viewportHeight,
	      presentation: this.#presentation,
	      managed: this.#managed(),
	      locked: this.#locked,
	      pinned: this.#pinned,
	      isDefault: !this.#geometryPersisted && !this.#locked && !this.#pinned
	    });
	  }
	  #commit() {
	    const next = this.#derive(), previous = this.#snapshot;
	    next.geometry.left === previous.geometry.left && next.geometry.top === previous.geometry.top && next.geometry.width === previous.geometry.width && next.geometry.height === previous.geometry.height && next.viewportWidth === previous.viewportWidth && next.viewportHeight === previous.viewportHeight && next.presentation.mode === previous.presentation.mode && next.locked === previous.locked && next.pinned === previous.pinned && next.isDefault === previous.isDefault || (this.#snapshot = next, this.changes.emit(next));
	  }
	}
	class ReaderWindowDomAdapter {
	  scope;
	  #model;
	  #overlay;
	  #modal;
	  #header;
	  #lockButton;
	  #pinButton;
	  #dispatchEvents;
	  #previousLocked = null;
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#overlay = options.overlay, this.#modal = options.modal, this.#header = options.header, this.#lockButton = options.lockButton, this.#pinButton = options.pinButton, this.#dispatchEvents = options.dispatchEvents !== !1, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#lockButton && this.scope.listen(this.#lockButton, "animationend", () => {
	      this.#lockButton?.classList.remove("ldp-lock-state-changing");
	    }), this.#model.changes.subscribe((snapshot) => this.#apply(snapshot, !0), this.scope), this.scope.add(() => this.#clear()), this.#apply(this.#model.snapshot, !1);
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #apply(snapshot, changed) {
	    if (!this.#destroyed) {
	      if (this.#overlay.classList.toggle("ldp-window-locked", snapshot.locked), this.#overlay.classList.toggle("ldp-window-pinned", snapshot.pinned), this.#overlay.classList.toggle("ldp-window-managed", snapshot.managed), this.#header?.toggleAttribute(
	        "data-ldp-reader-drag-surface",
	        snapshot.managed && !snapshot.locked
	      ), this.#header && (snapshot.managed && !snapshot.locked ? this.#header.dataset.ldpTooltipLabel = "按住空白处可拖动" : delete this.#header.dataset.ldpTooltipLabel), this.#syncWindowButtons(snapshot), snapshot.managed) {
	        const geometry = snapshot.geometry;
	        this.#modal.style.setProperty("left", `${geometry.left}px`), this.#modal.style.setProperty("top", `${geometry.top}px`), this.#modal.style.setProperty("width", `${geometry.width}px`), this.#modal.style.setProperty("height", `${geometry.height}px`), projectReaderWindowPlacement(this.#overlay, geometry);
	      } else
	        this.#clearGeometry();
	      changed && this.#dispatchEvents && dispatchCompatibilityEvent(this.#overlay, "ldp-reader-window-change");
	    }
	  }
	  #syncWindowButtons(snapshot) {
	    if (this.#lockButton) {
	      const label = snapshot.locked ? "解锁浮窗" : "锁定浮窗";
	      this.#lockButton.classList.toggle("active", snapshot.locked), this.#lockButton.setAttribute("aria-pressed", String(snapshot.locked)), this.#lockButton.setAttribute("aria-label", label), this.#lockButton.title = label, this.#lockButton.dataset.locked = String(snapshot.locked);
	      for (const icon of this.#lockButton.querySelectorAll(
	        "[data-reader-lock-icon]"
	      ))
	        icon.hidden = icon.dataset.readerLockIcon !== (snapshot.locked ? "locked" : "unlocked");
	      this.#previousLocked !== null && this.#previousLocked !== snapshot.locked && (this.#lockButton.classList.remove("ldp-lock-state-changing"), this.#lockButton.offsetWidth, this.#lockButton.classList.add("ldp-lock-state-changing"));
	    }
	    if (this.#previousLocked = snapshot.locked, this.#pinButton) {
	      const label = snapshot.pinned ? "恢复点击外部关闭" : "点击外部时保持显示";
	      this.#pinButton.classList.toggle("active", snapshot.pinned), this.#pinButton.setAttribute("aria-pressed", String(snapshot.pinned)), this.#pinButton.setAttribute("aria-label", label), this.#pinButton.title = label;
	    }
	  }
	  #clearGeometry() {
	    projectReaderWindowPlacement(this.#overlay, null);
	    for (const property of ["left", "top", "width", "height"])
	      this.#modal.style.removeProperty(property);
	  }
	  #clear() {
	    this.#overlay.classList.remove(
	      "ldp-window-locked",
	      "ldp-window-pinned",
	      "ldp-window-managed"
	    ), this.#header?.removeAttribute("data-ldp-reader-drag-surface"), this.#header && delete this.#header.dataset.ldpTooltipLabel, this.#lockButton?.classList.remove("active", "ldp-lock-state-changing"), this.#pinButton?.classList.remove("active"), this.#clearGeometry();
	  }
	}
	const WINDOW_DRAG_BLOCKED_SELECTOR = [
	  "a",
	  "button",
	  "input",
	  "select",
	  "textarea",
	  "label",
	  "summary",
	  '[role="button"]',
	  '[contenteditable="true"]',
	  ".ldp-title-jump",
	  ".ldp-meta-row",
	  ".ldp-title-topic-row",
	  ".ldp-notifications-popover",
	  ".ldp-history-popover",
	  ".ldp-bookmarks-popover",
	  ".ldp-settings-popover",
	  ".ldp-topic-edit-layer"
	].join(",");
	class ReaderWindowPointerController {
	  scope;
	  #model;
	  #overlay;
	  #modal;
	  #header;
	  #onPersist;
	  #requestFrame;
	  #cancelFrame;
	  #dragSurfaceSelector;
	  #blockedSelector;
	  #isDragBlocked;
	  #interactingClassName;
	  #restingTransform;
	  #projectPlacement;
	  #interaction = null;
	  #frame = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#model = options.model, this.#overlay = options.overlay, this.#modal = options.modal, this.#header = options.header, this.#onPersist = options.onPersist, this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.#dragSurfaceSelector = options.dragSurfaceSelector ?? ".ldp-header[data-ldp-reader-drag-surface]", this.#blockedSelector = options.blockedSelector ?? WINDOW_DRAG_BLOCKED_SELECTOR, this.#isDragBlocked = options.isDragBlocked, this.#interactingClassName = options.interactingClassName ?? "ldp-window-interacting", this.#restingTransform = options.restingTransform, this.#projectPlacement = options.projectPlacement ?? projectReaderWindowPlacement, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(this.#overlay, "pointerdown", (event) => this.#onPointerDown(
	      event
	    )), this.scope.listen(this.#overlay, "pointermove", (event) => this.#onPointerMove(
	      event
	    ));
	    for (const type of ["pointerup", "pointercancel"])
	      this.scope.listen(this.#overlay, type, (event) => this.#onPointerEnd(
	        event
	      ));
	    options.lockButton && this.scope.listen(options.lockButton, "click", (event) => {
	      event.preventDefault(), event.stopPropagation(), this.#stopInteraction(), this.#model.setLocked(!this.#model.snapshot.locked), this.#persist();
	    }), options.pinButton && this.scope.listen(options.pinButton, "click", (event) => {
	      event.preventDefault(), event.stopPropagation(), this.#model.setPinned(!this.#model.snapshot.pinned), this.#persist();
	    }), options.viewportTarget && options.readViewport && this.scope.listen(options.viewportTarget, "resize", () => {
	      this.#stopInteraction();
	      const viewport = options.readViewport();
	      this.#model.resizeViewport(viewport.width, viewport.height);
	    }), this.scope.add(() => this.#stopInteraction());
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #onPointerDown(event) {
	    if (this.#destroyed || event.button !== 0 || !this.#model.snapshot.managed || this.#model.snapshot.locked)
	      return;
	    const target = (0, import_event_target.eventElement)(event);
	    if (!target) return;
	    const resizeHandle = target.closest("[data-reader-resize]");
	    let mode = null, handle = null;
	    if (resizeHandle && this.#overlay.contains(resizeHandle)) {
	      const direction = resizeHandle.dataset.readerResize;
	      (direction === "n" || direction === "s" || direction === "e" || direction === "w" || direction === "nw" || direction === "ne" || direction === "sw" || direction === "se") && (mode = direction, handle = resizeHandle);
	    } else {
	      const dragSurface = target.closest(
	        this.#dragSurfaceSelector
	      );
	      dragSurface === this.#header && !target.closest(this.#blockedSelector) && !this.#isDragBlocked?.(event, target) && (mode = "drag", handle = dragSurface);
	    }
	    if (!mode || !handle) return;
	    event.preventDefault(), event.stopPropagation();
	    const start = this.#model.snapshot.geometry;
	    this.#interaction = {
	      pointerId: event.pointerId,
	      target: handle,
	      mode,
	      startX: event.clientX,
	      startY: event.clientY,
	      x: event.clientX,
	      y: event.clientY,
	      start,
	      preview: null
	    }, this.#overlay.classList.add(this.#interactingClassName), this.#overlay.dataset.readerWindowInteraction = mode, dispatchCompatibilityEvent(
	      this.#overlay,
	      "ldp-reader-window-interaction-start"
	    );
	    const pointerTarget = handle;
	    try {
	      pointerTarget.setPointerCapture?.(event.pointerId);
	    } catch {
	    }
	  }
	  #onPointerMove(event) {
	    !this.#interaction || event.pointerId !== this.#interaction.pointerId || (this.#interaction.x = event.clientX, this.#interaction.y = event.clientY, this.#frame || (this.#frame = this.#requestFrame(() => {
	      this.#frame = 0, this.#renderInteraction();
	    })));
	  }
	  #onPointerEnd(event) {
	    if (!this.#interaction || event.pointerId !== this.#interaction.pointerId) return;
	    this.#interaction.x = event.clientX, this.#interaction.y = event.clientY, this.#frame && (this.#cancelFrame(this.#frame), this.#frame = 0), this.#renderInteraction();
	    const interaction = this.#interaction;
	    if (interaction?.mode === "drag" && interaction.preview) {
	      const preview = interaction.preview;
	      this.#model.setGeometry(
	        preview.width,
	        preview.height,
	        preview.left,
	        preview.top
	      );
	    }
	    this.#stopInteraction(), this.#persist();
	  }
	  #renderInteraction() {
	    const interaction = this.#interaction;
	    if (!interaction || !this.#model.snapshot.managed) return;
	    const deltaX = interaction.x - interaction.startX, deltaY = interaction.y - interaction.startY;
	    if (interaction.mode === "drag") {
	      const preview = this.#model.previewGeometry(
	        interaction.start.width,
	        interaction.start.height,
	        interaction.start.left + deltaX,
	        interaction.start.top + deltaY
	      );
	      interaction.preview = preview, this.#modal.style.transform = `translate3d(${preview.left - interaction.start.left}px,${preview.top - interaction.start.top}px,0)`, this.#projectPlacement(this.#overlay, preview);
	      return;
	    }
	    const raw = this.#model.previewResize(
	      interaction.start,
	      interaction.mode,
	      deltaX,
	      deltaY
	    );
	    this.#model.setGeometry(raw.width, raw.height, raw.left, raw.top);
	  }
	  #stopInteraction() {
	    this.#frame && this.#cancelFrame(this.#frame), this.#frame = 0;
	    const interaction = this.#interaction;
	    if (this.#interaction = null, this.#overlay.classList.remove(this.#interactingClassName), this.#restingTransform === void 0 ? this.#modal.style.removeProperty("transform") : this.#modal.style.setProperty("transform", this.#restingTransform), !interaction) return;
	    const snapshot = this.#model.snapshot;
	    this.#projectPlacement(
	      this.#overlay,
	      snapshot.managed ? snapshot.geometry : null
	    );
	    const pointerTarget = interaction.target;
	    try {
	      pointerTarget.hasPointerCapture?.(interaction.pointerId) && pointerTarget.releasePointerCapture?.(interaction.pointerId);
	    } catch {
	    }
	    dispatchCompatibilityEvent(
	      this.#overlay,
	      "ldp-reader-window-interaction-end"
	    ), delete this.#overlay.dataset.readerWindowInteraction;
	  }
	  #persist() {
	    this.#onPersist?.(this.#model.preferencePatch());
	  }
	}
}, "48837eddefba6208c03437dfceea81ab115842737f6592b31ac086792edb76c1");

/* Source: lite/src/state/preferences-config-codec.ts */
runtime.register("src/state/preferences-config-codec.js", function(module, exports, require) {
	var preferences_config_codec_exports = {};
	__export(preferences_config_codec_exports, {
	  PreferencesConfigCodec: () => PreferencesConfigCodec
	});
	module.exports = __toCommonJS(preferences_config_codec_exports);
	function nonEmpty(value, name) {
	  const normalized = String(value ?? "").trim();
	  if (!normalized) throw new Error(`${name} 不能为空`);
	  return normalized;
	}
	function plainRecord(value, name) {
	  if (!value || typeof value != "object" || Array.isArray(value))
	    throw new Error(`${name} 必须是对象`);
	  return value;
	}
	function exactKeys(value, expected) {
	  const actual = Object.keys(value).sort(), canonical = [...expected].sort();
	  return actual.length === canonical.length && actual.every((key, index) => key === canonical[index]);
	}
	class PreferencesConfigCodec {
	  #format;
	  #schemaVersion;
	  #scriptVersion;
	  #defaults;
	  #normalize;
	  #legacyRules;
	  #settingKeys;
	  #settingKeySet;
	  #now;
	  constructor(options) {
	    if (this.#format = nonEmpty(options.format, "config format"), !Number.isSafeInteger(options.schemaVersion) || options.schemaVersion < 1)
	      throw new RangeError("config schemaVersion 必须是正安全整数");
	    if (this.#schemaVersion = options.schemaVersion, this.#scriptVersion = nonEmpty(options.scriptVersion, "scriptVersion"), this.#defaults = options.defaults, this.#normalize = options.normalize, this.#settingKeys = Object.freeze(Object.keys(options.defaults)), !this.#settingKeys.length) throw new Error("preferences defaults 不能为空");
	    this.#settingKeySet = new Set(this.#settingKeys), this.#legacyRules = Object.freeze((options.legacyImportRules ?? []).map((rule) => {
	      const missingDefaults = plainRecord(rule.missingDefaults, "legacy missingDefaults"), keys = Object.keys(missingDefaults);
	      if (!keys.length || keys.some((key) => !this.#settingKeySet.has(key)))
	        throw new Error("legacy missingDefaults 必须只包含已知偏好字段");
	      return Object.freeze({ missingDefaults: Object.freeze({ ...missingDefaults }) });
	    })), this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
	  }
	  get settingKeys() {
	    return this.#settingKeys;
	  }
	  export(value) {
	    const normalized = this.#normalize({
	      ...this.#defaults,
	      ...value
	    }), settings = Object.freeze(Object.fromEntries(
	      this.#settingKeys.map((key) => [key, normalized[key]])
	    ));
	    return Object.freeze({
	      format: this.#format,
	      schemaVersion: this.#schemaVersion,
	      scriptVersion: this.#scriptVersion,
	      exportedAt: this.#now().toISOString(),
	      settingsCount: this.#settingKeys.length,
	      settings
	    });
	  }
	  import(payload) {
	    try {
	      return this.#import(payload);
	    } catch (cause) {
	      throw cause instanceof Error && cause.message === "invalid_config" ? cause : new Error("invalid_config", { cause });
	    }
	  }
	  #import(payload) {
	    const record = plainRecord(payload, "config payload");
	    if (!exactKeys(record, [
	      "format",
	      "schemaVersion",
	      "scriptVersion",
	      "exportedAt",
	      "settingsCount",
	      "settings"
	    ]) || record.format !== this.#format || record.schemaVersion !== this.#schemaVersion || typeof record.scriptVersion != "string" || !record.scriptVersion.trim() || typeof record.exportedAt != "string" || !record.exportedAt.trim())
	      throw new Error("invalid_config");
	    const settingsRecord = plainRecord(record.settings, "config settings"), originalKeys = Object.keys(settingsRecord);
	    if (record.settingsCount !== originalKeys.length || originalKeys.some((key) => !this.#settingKeySet.has(key)))
	      throw new Error("invalid_config");
	    const migrated = { ...settingsRecord };
	    for (const rule of this.#legacyRules) {
	      const allowedMissing = new Set(Object.keys(rule.missingDefaults));
	      if (this.#settingKeys.every((key) => Object.hasOwn(migrated, key) || allowedMissing.has(key)))
	        for (const [key, value] of Object.entries(rule.missingDefaults))
	          Object.hasOwn(migrated, key) || (migrated[key] = value);
	    }
	    if (this.#settingKeys.some((key) => !Object.hasOwn(migrated, key)))
	      throw new Error("invalid_config");
	    const selected = Object.fromEntries(
	      this.#settingKeys.map((key) => [key, migrated[key]])
	    );
	    return this.#normalize({
	      ...this.#defaults,
	      ...selected
	    });
	  }
	}
}, "11cea2ff96a9bf9261c17b5eba9846e468cb5fd87580df681664e704f1b7191b");

/* Source: lite/src/state/preferences-repository.ts */
runtime.register("src/state/preferences-repository.js", function(module, exports, require) {
	var preferences_repository_exports = {};
	__export(preferences_repository_exports, {
	  PreferencesRepository: () => PreferencesRepository
	});
	module.exports = __toCommonJS(preferences_repository_exports);
	var import_signal = require("../kernel/signal.js");
	function plainRecord(value) {
	  if (!value || typeof value != "object" || Array.isArray(value))
	    throw new TypeError("偏好存储值必须是对象");
	  return value;
	}
	function nonEmptyKey(value) {
	  const key = String(value).trim();
	  if (!key) throw new Error("preferences storage key 不能为空");
	  return key;
	}
	function freezePreferenceValue(value, seen = /* @__PURE__ */ new WeakSet()) {
	  if (!value || typeof value != "object") return value;
	  const object = value;
	  if (seen.has(object)) return value;
	  if (seen.add(object), Array.isArray(value)) {
	    for (const entry of value) freezePreferenceValue(entry, seen);
	    return Object.freeze(value);
	  }
	  const prototype = Object.getPrototypeOf(value);
	  if (prototype !== Object.prototype && prototype !== null) return value;
	  for (const entry of Object.values(value)) freezePreferenceValue(entry, seen);
	  return Object.freeze(value);
	}
	class PreferencesRepository {
	  changes = new import_signal.Signal();
	  diagnostics = new import_signal.Signal();
	  #key;
	  #storage;
	  #defaults;
	  #normalize;
	  #prepareStored;
	  #serialize;
	  #parse;
	  #snapshot;
	  constructor(options) {
	    this.#key = nonEmptyKey(options.key), this.#storage = options.storage, this.#normalize = options.normalize, this.#prepareStored = options.prepareStored ?? ((input) => input), this.#serialize = options.serialize ?? JSON.stringify, this.#parse = options.parse ?? JSON.parse, this.#defaults = freezePreferenceValue({
	      ...this.#normalize({ ...options.defaults })
	    }), this.#snapshot = Object.freeze({
	      value: this.#defaults,
	      revision: 0,
	      source: "fallback"
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  load() {
	    return this.#readAndCommit("initial");
	  }
	  reloadExternal() {
	    return this.#readAndCommit("external-reload");
	  }
	  update(patch) {
	    return this.#persistAndCommit(
	      this.#normalize({
	        ...this.#snapshot.value,
	        ...patch
	      }),
	      "local-update"
	    );
	  }
	  replace(value) {
	    return this.#persistAndCommit(
	      this.#normalize({
	        ...this.#defaults,
	        ...value
	      }),
	      "replace"
	    );
	  }
	  #readAndCommit(source) {
	    let value = this.#defaults, commitSource = source;
	    try {
	      const stored = this.#storage.getItem(this.#key);
	      if (stored !== null) {
	        const parsed = plainRecord(this.#parse(stored)), prepared = plainRecord(this.#prepareStored(parsed));
	        value = this.#normalize({
	          ...this.#defaults,
	          ...prepared
	        });
	      }
	    } catch (cause) {
	      commitSource = "fallback", this.diagnostics.emit(Object.freeze({
	        code: cause instanceof TypeError ? "invalid-stored-value" : "read-failed",
	        cause
	      }));
	    }
	    return this.#commit(value, commitSource);
	  }
	  #persistAndCommit(value, source) {
	    const frozen = freezePreferenceValue({ ...value });
	    try {
	      this.#storage.setItem(this.#key, this.#serialize(frozen));
	    } catch (cause) {
	      throw this.diagnostics.emit(Object.freeze({ code: "write-failed", cause })), cause;
	    }
	    return this.#commit(frozen, source);
	  }
	  #commit(value, source) {
	    const snapshot = Object.freeze({
	      value: freezePreferenceValue({ ...value }),
	      revision: this.#snapshot.revision + 1,
	      source
	    });
	    return this.#snapshot = snapshot, this.changes.emit(snapshot), snapshot;
	  }
	}
}, "7dbda24beac5a9c311c3653615cadbc783aa23e655c1baf2fd5fa146fefbf7b4");

/* Source: lite/src/state/reader-account-scoped-storage.ts */
runtime.register("src/state/reader-account-scoped-storage.js", function(module, exports, require) {
	var reader_account_scoped_storage_exports = {};
	__export(reader_account_scoped_storage_exports, {
	  readReaderAccountScopedString: () => readReaderAccountScopedString,
	  readReaderAccountScopedValue: () => readReaderAccountScopedValue,
	  readerAccountScopedStorageIdentity: () => readerAccountScopedStorageIdentity
	});
	module.exports = __toCommonJS(reader_account_scoped_storage_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function baseStorageKey(value) {
	  const normalized = String(value ?? "").trim();
	  if (!normalized) throw new Error("account scoped storage base key 不能为空");
	  return normalized;
	}
	function missing(value) {
	  return value == null;
	}
	function readerAccountScopedStorageIdentity(legacyKeyValue, authScopeValue) {
	  const legacyKey = baseStorageKey(legacyKeyValue), authScope = (0, import_identifiers.discourseAuthScope)(authScopeValue);
	  return Object.freeze({
	    authScope,
	    key: `${legacyKey}:scope:v2:${encodeURIComponent(authScope)}`,
	    legacyKey,
	    legacyOwnerKey: `${legacyKey}:legacy-owner:v2`,
	    canClaimLegacy: authScope.startsWith("account:")
	  });
	}
	function readReaderAccountScopedString(storage, identity) {
	  const scoped = storage.getItem(identity.key);
	  if (scoped !== null) return scoped;
	  if (!identity.canClaimLegacy) return null;
	  const legacy = storage.getItem(identity.legacyKey);
	  if (legacy === null) return null;
	  const owner = storage.getItem(identity.legacyOwnerKey);
	  return owner !== null && owner !== identity.authScope || owner === null && (storage.setItem(identity.legacyOwnerKey, identity.authScope), storage.getItem(identity.legacyOwnerKey) !== identity.authScope) ? null : (storage.setItem(identity.key, legacy), storage.getItem(identity.key));
	}
	async function readReaderAccountScopedValue(storage, identity) {
	  const scoped = await storage.getValue(identity.key);
	  if (!missing(scoped)) return scoped;
	  if (!identity.canClaimLegacy) return null;
	  const legacy = await storage.getValue(identity.legacyKey);
	  if (missing(legacy)) return null;
	  const owner = await storage.getValue(identity.legacyOwnerKey);
	  if (!missing(owner) && String(owner) !== identity.authScope || missing(owner) && (await storage.setValue(identity.legacyOwnerKey, identity.authScope), String(await storage.getValue(identity.legacyOwnerKey)) !== identity.authScope))
	    return null;
	  await storage.setValue(identity.key, legacy);
	  const migrated = await storage.getValue(identity.key);
	  return missing(migrated) ? null : migrated;
	}
}, "4ef3eb13ce397fec240b7b3fa2c8798d30a38f2ec4b11956c44131fd85e18f23");

/* Source: lite/src/state/reader-boost-copy-settings.ts */
runtime.register("src/state/reader-boost-copy-settings.js", function(module, exports, require) {
	var reader_boost_copy_settings_exports = {};
	__export(reader_boost_copy_settings_exports, {
	  BOOST_COPY_MAX_LENGTH: () => BOOST_COPY_MAX_LENGTH,
	  DEFAULT_BOOST_COPY_SETTINGS: () => DEFAULT_BOOST_COPY_SETTINGS,
	  normalizeBoostCopySettings: () => normalizeBoostCopySettings
	});
	module.exports = __toCommonJS(reader_boost_copy_settings_exports);
	const BOOST_COPY_MAX_LENGTH = 16, DEFAULT_BOOST_COPY_SETTINGS = Object.freeze({
	  mode: "counter",
	  prefix: "",
	  counterMarker: "+",
	  counterStep: 1,
	  fixedSuffix: ""
	});
	function boundedText(value, fallback = "") {
	  return [...String(value ?? fallback).replace(/\s+/g, " ")].slice(0, 16).join("");
	}
	function normalizeBoostCopySettings(input) {
	  const source = input ?? {}, markerInput = boundedText(
	    source.counterMarker,
	    DEFAULT_BOOST_COPY_SETTINGS.counterMarker
	  ).trim(), marker = markerInput && !/\d$/.test(markerInput) ? markerInput : DEFAULT_BOOST_COPY_SETTINGS.counterMarker, step = Number(source.counterStep);
	  return Object.freeze({
	    mode: source.mode === "text" ? "text" : "counter",
	    prefix: boundedText(source.prefix),
	    counterMarker: marker,
	    counterStep: Number.isFinite(step) ? Math.min(99, Math.max(1, Math.round(step))) : DEFAULT_BOOST_COPY_SETTINGS.counterStep,
	    fixedSuffix: boundedText(source.fixedSuffix)
	  });
	}
}, "b53c781301e282e8c6d15cf16425640164ea8d4d6fd06570f325d605596228d3");

/* Source: lite/src/state/reader-preferences-schema.ts */
runtime.register("src/state/reader-preferences-schema.js", function(module, exports, require) {
	var reader_preferences_schema_exports = {};
	__export(reader_preferences_schema_exports, {
	  IMAGE_PROFILE_DEFAULT: () => IMAGE_PROFILE_DEFAULT,
	  LIGHTBOX_COMMENTS_WIDTH_DEFAULT: () => LIGHTBOX_COMMENTS_WIDTH_DEFAULT,
	  LIGHTBOX_COMMENTS_WIDTH_MAX: () => LIGHTBOX_COMMENTS_WIDTH_MAX,
	  LIGHTBOX_COMMENTS_WIDTH_MIN: () => LIGHTBOX_COMMENTS_WIDTH_MIN,
	  LIGHTBOX_DESCRIPTION_HEIGHT_DEFAULT: () => LIGHTBOX_DESCRIPTION_HEIGHT_DEFAULT,
	  LIGHTBOX_DESCRIPTION_HEIGHT_MIN: () => LIGHTBOX_DESCRIPTION_HEIGHT_MIN,
	  READER_APPEARANCE_COLOR_NAMES: () => READER_APPEARANCE_COLOR_NAMES,
	  READER_APPEARANCE_DEFAULT: () => READER_APPEARANCE_DEFAULT,
	  READER_APPEARANCE_NUMERIC_LIMITS: () => READER_APPEARANCE_NUMERIC_LIMITS,
	  READER_APPEARANCE_SETTING_NAMES: () => READER_APPEARANCE_SETTING_NAMES,
	  READER_CONFIG_EXPORT_FORMAT: () => READER_CONFIG_EXPORT_FORMAT,
	  READER_CONFIG_EXPORT_VERSION: () => READER_CONFIG_EXPORT_VERSION,
	  READER_FONT_DEFAULT: () => READER_FONT_DEFAULT,
	  READER_FONT_FAMILIES: () => READER_FONT_FAMILIES,
	  READER_FONT_SCALE_LIMITS: () => READER_FONT_SCALE_LIMITS,
	  READER_FONT_WEIGHTS: () => READER_FONT_WEIGHTS,
	  READER_FULLPAGE_LAYOUT_DEFAULT: () => READER_FULLPAGE_LAYOUT_DEFAULT,
	  READER_HOST_FONT_SCALE_DEFAULTS: () => READER_HOST_FONT_SCALE_DEFAULTS,
	  READER_HOST_FONT_SCALE_LIMITS: () => READER_HOST_FONT_SCALE_LIMITS,
	  READER_JUMP_HIGHLIGHT_DEFAULTS: () => READER_JUMP_HIGHLIGHT_DEFAULTS,
	  READER_JUMP_HIGHLIGHT_LIMITS: () => READER_JUMP_HIGHLIGHT_LIMITS,
	  READER_LAYOUT_DEFAULT: () => READER_LAYOUT_DEFAULT,
	  READER_LAYOUT_MINIMUM_RATIOS: () => READER_LAYOUT_MINIMUM_RATIOS,
	  READER_LAYOUT_REGIONS: () => READER_LAYOUT_REGIONS,
	  READER_LOADING_ANIMATION_KEYS: () => READER_LOADING_ANIMATION_KEYS,
	  READER_PERFORMANCE_LIMITS: () => READER_PERFORMANCE_LIMITS,
	  READER_PERFORMANCE_PRESETS: () => READER_PERFORMANCE_PRESETS,
	  READER_PREFERENCES_STORAGE_KEY: () => READER_PREFERENCES_STORAGE_KEY,
	  READER_SHORTCUT_ACTIONS: () => READER_SHORTCUT_ACTIONS,
	  READER_SHORTCUT_DEFAULTS: () => READER_SHORTCUT_DEFAULTS,
	  createReaderPerformancePreferencesPatch: () => createReaderPerformancePreferencesPatch,
	  createReaderPreferencesConfigCodec: () => createReaderPreferencesConfigCodec,
	  createReaderPreferencesDefaults: () => createReaderPreferencesDefaults,
	  createReaderPreferencesRepository: () => createReaderPreferencesRepository,
	  normalizeImageProfile: () => normalizeImageProfile,
	  normalizeReaderAppearanceProfile: () => normalizeReaderAppearanceProfile,
	  normalizeReaderFontProfile: () => normalizeReaderFontProfile,
	  normalizeReaderPreferences: () => normalizeReaderPreferences,
	  normalizeReaderShortcutBinding: () => normalizeReaderShortcutBinding,
	  normalizeReaderShortcutBindings: () => normalizeReaderShortcutBindings,
	  prepareStoredReaderPreferences: () => prepareStoredReaderPreferences,
	  readReaderPerformanceConfig: () => readReaderPerformanceConfig,
	  readerAppearanceEditableProfile: () => readerAppearanceEditableProfile,
	  readerLayoutProfileTotal: () => readerLayoutProfileTotal,
	  readerLayoutRegionMaximum: () => readerLayoutRegionMaximum,
	  readerPerformancePresetForConfig: () => readerPerformancePresetForConfig,
	  readerShortcutBindingPolicyIssue: () => readerShortcutBindingPolicyIssue,
	  rebalanceReaderLayoutProfile: () => rebalanceReaderLayoutProfile,
	  resolveReaderAppearanceColor: () => resolveReaderAppearanceColor
	});
	module.exports = __toCommonJS(reader_preferences_schema_exports);
	var import_preferences_config_codec = require("./preferences-config-codec.js"), import_preferences_repository = require("./preferences-repository.js"), import_reader_boost_copy_settings = require("./reader-boost-copy-settings.js"), import_reader_translation_presentation = require("../translation/reader-translation-presentation.js"), import_reader_unwanted_topic_filter = require("../collection/reader-unwanted-topic-filter.js");
	const READER_PREFERENCES_STORAGE_KEY = "linuxdo-enhanced-reader:prefs", READER_CONFIG_EXPORT_FORMAT = "awesome-linuxdo-reader-settings", READER_CONFIG_EXPORT_VERSION = 5, IMAGE_SCALE_OPTIONS = Object.freeze([25, 50, 100, 125, 150, 200]), LIGHTBOX_DESCRIPTION_HEIGHT_MIN = 56, LIGHTBOX_DESCRIPTION_HEIGHT_DEFAULT = 120, LIGHTBOX_COMMENTS_WIDTH_DEFAULT = 25, LIGHTBOX_COMMENTS_WIDTH_MIN = 18, LIGHTBOX_COMMENTS_WIDTH_MAX = 50, IMAGE_SCALE_MIN = 50, IMAGE_SCALE_MAX = 200, FONT_SCALE_MIN = 50, FONT_SCALE_MAX = 250, COMPOSER_FONT_SCALE_DEFAULT = 80, HOST_EMBED_SIZE_MIN = 50, HOST_EMBED_SIZE_MAX = 200, READER_WINDOW_MIN_WIDTH = 360, READER_WINDOW_MIN_HEIGHT = 320, READER_EMBED_MIN_WIDTH = 360, HISTORY_EDGE_TRIGGER_MIN = 0, HISTORY_EDGE_TRIGGER_MAX = 15, HISTORY_EDGE_TRIGGER_DEFAULT = 15, INLINE_REPLY_TREE_DEFAULT_DEPTH = 3, INLINE_REPLY_TREE_MAX_DEPTH = 5, FONT_FAMILIES = /* @__PURE__ */ new Set([
	  "site",
	  "system",
	  "cjkSans",
	  "serif",
	  "monospace",
	  "custom"
	]), FONT_WEIGHTS = /* @__PURE__ */ new Set([300, 400, 500, 600]), BOOKMARK_TAB_TYPES = Object.freeze([
	  "Reply",
	  "Boost",
	  "Reaction",
	  "Topic",
	  "Post"
	]), READER_LOADING_ANIMATION_KEYS = Object.freeze([
	  "portal",
	  "constellation",
	  "corridor",
	  "typewave",
	  "crystal",
	  "marginalia",
	  "chapters",
	  "quoteecho",
	  "footnotes",
	  "inkverse"
	]), READER_LOADING_ANIMATIONS = new Set(
	  READER_LOADING_ANIMATION_KEYS
	), LAYOUT_KEYS = Object.freeze([
	  "left",
	  "main",
	  "gap",
	  "timeline",
	  "right"
	]), READER_LAYOUT_DEFAULT = Object.freeze({
	  left: 0,
	  main: 88,
	  gap: 0,
	  timeline: 8,
	  right: 4
	}), READER_FULLPAGE_LAYOUT_DEFAULT = Object.freeze({
	  left: 15,
	  main: 70,
	  gap: 5,
	  timeline: 8,
	  right: 2
	}), LAYOUT_MIN_RATIOS = Object.freeze({
	  left: 0,
	  main: 40,
	  gap: 0,
	  timeline: 6,
	  right: 0
	}), READER_LAYOUT_REGIONS = LAYOUT_KEYS, READER_LAYOUT_MINIMUM_RATIOS = LAYOUT_MIN_RATIOS, READER_FONT_DEFAULT = Object.freeze({
	  family: "system",
	  customFamily: "",
	  weight: 400,
	  interfaceColor: "",
	  interface: 92,
	  postFamily: "system",
	  postCustomFamily: "",
	  postWeight: 400,
	  postColor: "",
	  post: 95,
	  composerFamily: "system",
	  composerCustomFamily: "",
	  composerWeight: 400,
	  composerColor: "",
	  composer: COMPOSER_FONT_SCALE_DEFAULT
	}), READER_FONT_FAMILIES = Object.freeze([...FONT_FAMILIES]), READER_FONT_WEIGHTS = Object.freeze([...FONT_WEIGHTS]), READER_FONT_SCALE_LIMITS = Object.freeze({
	  min: FONT_SCALE_MIN,
	  max: FONT_SCALE_MAX,
	  step: 1
	}), READER_HOST_FONT_SCALE_LIMITS = Object.freeze({
	  min: HOST_EMBED_SIZE_MIN,
	  max: HOST_EMBED_SIZE_MAX,
	  step: 1
	}), READER_HOST_FONT_SCALE_DEFAULTS = Object.freeze({
	  title: 110,
	  avatar: 80,
	  stats: 120,
	  labelCard: 100
	}), IMAGE_PROFILE_DEFAULT = Object.freeze({
	  preset: "100",
	  custom: 100
	}), APPEARANCE_PROFILE_DEFAULT = Object.freeze({
	  accentColor: "#47855f",
	  accentColorDark: "#78c295",
	  linkColor: "#2870b8",
	  linkColorDark: "#71b7ff",
	  zebraColor: "#f7f7f7",
	  zebraColorDark: "#1b2b21",
	  zebraRadius: 10,
	  listZebraColor: "#f7f7f7",
	  listZebraColorDark: "#242a31",
	  structureColorsEnabled: !0,
	  replyLineColor: "#6dab85",
	  replyLineColorDark: "#78c295",
	  replyLineWidth: 1,
	  replyLineRadius: 15,
	  quoteLineColor: "#d7d7d7",
	  quoteLineColorDark: "#46505a",
	  quoteLineWidth: 0.5,
	  dividerLineColor: "#e5e5e5",
	  dividerLineColorDark: "#343b44",
	  dividerLineWidth: 0.5
	}), READER_APPEARANCE_DEFAULT = APPEARANCE_PROFILE_DEFAULT, READER_APPEARANCE_COLOR_NAMES = Object.freeze([
	  "accentColor",
	  "linkColor",
	  "zebraColor",
	  "listZebraColor",
	  "replyLineColor",
	  "quoteLineColor",
	  "dividerLineColor"
	]), READER_APPEARANCE_SETTING_NAMES = Object.freeze([
	  "accentColor",
	  "linkColor",
	  "zebraColor",
	  "zebraRadius",
	  "listZebraColor",
	  "structureColorsEnabled",
	  "replyLineColor",
	  "replyLineWidth",
	  "replyLineRadius",
	  "quoteLineColor",
	  "quoteLineWidth",
	  "dividerLineColor",
	  "dividerLineWidth"
	]), READER_APPEARANCE_NUMERIC_LIMITS = Object.freeze({
	  zebraRadius: Object.freeze({ min: 0, max: 16, step: 1 }),
	  replyLineWidth: Object.freeze({ min: 0.5, max: 4, step: 0.5 }),
	  replyLineRadius: Object.freeze({ min: 0, max: 16, step: 1 }),
	  quoteLineWidth: Object.freeze({ min: 0.5, max: 4, step: 0.5 }),
	  dividerLineWidth: Object.freeze({ min: 0.5, max: 4, step: 0.5 })
	}), READER_APPEARANCE_THEME_LIMITS = Object.freeze({
	  accentColor: Object.freeze({ saturationMax: 82, light: [30, 55], dark: [58, 78] }),
	  linkColor: Object.freeze({ saturationMax: 90, light: [30, 55], dark: [60, 80] }),
	  zebraColor: Object.freeze({ saturationMax: 70, light: [92, 100], dark: [10, 20] }),
	  listZebraColor: Object.freeze({ saturationMax: 38, light: [88, 98], dark: [10, 20] }),
	  replyLineColor: Object.freeze({ saturationMax: 76, light: [30, 58], dark: [55, 78] }),
	  quoteLineColor: Object.freeze({ saturationMax: 50, light: [72, 92], dark: [22, 42] }),
	  dividerLineColor: Object.freeze({ saturationMax: 40, light: [80, 94], dark: [18, 34] })
	}), PERFORMANCE_PRESETS = Object.freeze({
	  low: Object.freeze({
	    pageSize: 24,
	    streamOverscanViewports: 1,
	    streamMaxItems: 48,
	    nestedPrefetchViewports: 1.25,
	    requestMaxConcurrent: 2,
	    requestMinInterval: 180,
	    requestRateTarget: 75
	  }),
	  balanced: Object.freeze({
	    pageSize: 48,
	    streamOverscanViewports: 1.5,
	    streamMaxItems: 80,
	    nestedPrefetchViewports: 2.5,
	    requestMaxConcurrent: 3,
	    requestMinInterval: 100,
	    requestRateTarget: 85
	  }),
	  high: Object.freeze({
	    pageSize: 64,
	    streamOverscanViewports: 2,
	    streamMaxItems: 96,
	    nestedPrefetchViewports: 3,
	    requestMaxConcurrent: 4,
	    requestMinInterval: 80,
	    requestRateTarget: 90
	  })
	}), PERFORMANCE_NAMES = Object.freeze([
	  "pageSize",
	  "streamOverscanViewports",
	  "streamMaxItems",
	  "nestedPrefetchViewports",
	  "requestMaxConcurrent",
	  "requestMinInterval",
	  "requestRateTarget"
	]), PERFORMANCE_LIMITS = Object.freeze({
	  pageSize: Object.freeze({ min: 12, max: 64, integer: !0 }),
	  streamOverscanViewports: Object.freeze({ min: 0.25, max: 3 }),
	  streamMaxItems: Object.freeze({ min: 24, max: 128, integer: !0 }),
	  nestedPrefetchViewports: Object.freeze({ min: 1, max: 3 }),
	  requestMaxConcurrent: Object.freeze({ min: 1, max: 4, integer: !0 }),
	  requestMinInterval: Object.freeze({ min: 80, max: 500, integer: !0 }),
	  requestRateTarget: Object.freeze({ min: 50, max: 95, integer: !0 })
	}), READER_PERFORMANCE_PRESETS = PERFORMANCE_PRESETS, READER_PERFORMANCE_LIMITS = PERFORMANCE_LIMITS, READER_SHORTCUT_DEFAULTS = Object.freeze({
	  historyBack: Object.freeze(["ArrowLeft", "Mouse3"]),
	  historyForward: Object.freeze(["ArrowRight", "Mouse4"]),
	  topicTop: Object.freeze(["Home"]),
	  topicBottom: Object.freeze(["End"]),
	  floorJump: Object.freeze([]),
	  discussionHorizontalScroll: Object.freeze(["Shift+Wheel"]),
	  onlyAuthor: Object.freeze([]),
	  translate: Object.freeze([]),
	  refreshTopic: Object.freeze([]),
	  refreshHost: Object.freeze(["F5"]),
	  openOriginal: Object.freeze([]),
	  settings: Object.freeze(["Ctrl+Comma"]),
	  notifications: Object.freeze([]),
	  historyPanel: Object.freeze([]),
	  bookmarksPanel: Object.freeze([]),
	  likeTopic: Object.freeze([]),
	  replyTopic: Object.freeze([]),
	  bookmarkTopic: Object.freeze([]),
	  toggleFullscreen: Object.freeze([]),
	  toggleQueue: Object.freeze([]),
	  closeReader: Object.freeze(["Escape"])
	}), READER_SHORTCUT_ACTIONS = Object.freeze(
	  Object.keys(READER_SHORTCUT_DEFAULTS)
	), SHORTCUT_MODIFIERS = Object.freeze(["Ctrl", "Alt", "Shift", "Meta"]), READER_SHORTCUT_RESERVED_BINDINGS = /* @__PURE__ */ new Set([
	  "Ctrl+KeyD",
	  "Ctrl+KeyF",
	  "Ctrl+KeyH",
	  "Ctrl+KeyJ",
	  "Ctrl+KeyL",
	  "Ctrl+KeyN",
	  "Ctrl+KeyO",
	  "Ctrl+KeyP",
	  "Ctrl+KeyR",
	  "Ctrl+KeyS",
	  "Ctrl+KeyT",
	  "Ctrl+KeyW",
	  "Ctrl+Tab",
	  "Ctrl+Shift+KeyN",
	  "Ctrl+Shift+KeyB",
	  "Ctrl+Shift+KeyD",
	  "Ctrl+Shift+KeyI",
	  "Ctrl+Shift+KeyJ",
	  "Ctrl+Shift+KeyO",
	  "Ctrl+Shift+KeyP",
	  "Ctrl+Shift+KeyT",
	  "Ctrl+Shift+KeyW",
	  "Ctrl+Shift+Delete",
	  "Ctrl+Shift+Tab",
	  "Ctrl+KeyU",
	  "Alt+ArrowLeft",
	  "Alt+ArrowRight",
	  "Alt+F4",
	  "Alt+Home",
	  "Meta+Comma",
	  "Meta+KeyF",
	  "Meta+KeyL",
	  "Meta+KeyN",
	  "Meta+KeyP",
	  "Meta+KeyQ",
	  "Meta+KeyR",
	  "Meta+KeyS",
	  "Meta+KeyT",
	  "Meta+KeyW",
	  "Meta+BracketLeft",
	  "Meta+BracketRight",
	  "Alt+Meta+KeyC",
	  "Alt+Meta+KeyI",
	  "Alt+Meta+KeyJ",
	  "Alt+Meta+ArrowLeft",
	  "Alt+Meta+ArrowRight",
	  "Shift+Meta+BracketLeft",
	  "Shift+Meta+BracketRight",
	  "Shift+Meta+KeyN",
	  "Shift+Meta+KeyT",
	  "Shift+Meta+KeyW",
	  "F11",
	  "F12"
	]), READER_SHORTCUT_KEYBOARD_CODE = new RegExp([
	  "^(?:Key[A-Z]|Digit[0-9]|F(?:[1-9]|1[0-9]|2[0-4])|",
	  "Arrow(?:Down|Left|Right|Up)|",
	  "(?:Backquote|Backslash|BracketLeft|BracketRight|Comma|Equal|Minus|",
	  "Period|Quote|Semicolon|Slash)|",
	  "(?:Backspace|CapsLock|ContextMenu|Delete|End|Enter|Escape|Help|Home|",
	  "Insert|PageDown|PageUp|Pause|PrintScreen|ScrollLock|Space|Tab)|",
	  "Intl(?:Backslash|Ro|Yen)|Lang[1-5]|(?:Convert|KanaMode|NonConvert)|",
	  "Numpad(?:[0-9]|Add|Backspace|Clear|ClearEntry|Comma|Decimal|Divide|",
	  "Enter|Equal|Hash|MemoryAdd|MemoryClear|MemoryRecall|MemoryStore|",
	  "MemorySubtract|Multiply|ParenLeft|ParenRight|Star|Subtract)|",
	  "Browser(?:Back|Favorites|Forward|Home|Refresh|Search|Stop)|",
	  "Media(?:PlayPause|Select|Stop|TrackNext|TrackPrevious)|",
	  "AudioVolume(?:Down|Mute|Up)|Launch(?:App1|App2|Mail)|",
	  "(?:Abort|Again|Copy|Cut|Eject|Find|Fn|FnLock|Hyper|Open|Paste|Power|",
	  "Props|Select|Sleep|Super|Turbo|Undo|WakeUp))$"
	].join("")), JUMP_HIGHLIGHT_DEFAULTS = Object.freeze({
	  color: "#0888cc",
	  radius: 10,
	  borderWidth: 1,
	  rate: 0.8,
	  count: 1
	}), JUMP_HIGHLIGHT_LIMITS = Object.freeze({
	  radius: Object.freeze({ min: 0, max: 24, step: 1, integer: !0 }),
	  borderWidth: Object.freeze({ min: 0, max: 4, step: 1, integer: !0 }),
	  rate: Object.freeze({ min: 0.5, max: 2, step: 0.1, integer: !1 }),
	  count: Object.freeze({ min: 1, max: 6, step: 1, integer: !0 })
	}), READER_JUMP_HIGHLIGHT_DEFAULTS = JUMP_HIGHLIGHT_DEFAULTS, READER_JUMP_HIGHLIGHT_LIMITS = JUMP_HIGHLIGHT_LIMITS;
	function finiteViewport(value, name) {
	  if (!Number.isFinite(value) || value < 0)
	    throw new RangeError(`${name} 必须是非负有限数`);
	  return value;
	}
	function plainRecord(value) {
	  return value && typeof value == "object" && !Array.isArray(value) ? value : {};
	}
	function normalizeTopicActionRailPosition(value) {
	  const source = plainRecord(value), rawX = source.x, numericX = Number(rawX), x = rawX === "left" || rawX === "right" ? rawX : Number.isFinite(numericX) ? Math.max(0, Math.min(1, numericX)) : "left", numericY = Number(source.y);
	  return Object.freeze({
	    x,
	    y: Number.isFinite(numericY) ? Math.max(0, Math.min(1, numericY)) : 0.95
	  });
	}
	function normalizeTopicActionRailPositions(value, legacyValue) {
	  const source = plainRecord(value), legacy = normalizeTopicActionRailPosition(legacyValue);
	  return Object.freeze({
	    floating: Object.hasOwn(source, "floating") ? normalizeTopicActionRailPosition(source.floating) : legacy,
	    fullpage: Object.hasOwn(source, "fullpage") ? normalizeTopicActionRailPosition(source.fullpage) : legacy,
	    embedded: Object.hasOwn(source, "embedded") ? normalizeTopicActionRailPosition(source.embedded) : legacy
	  });
	}
	function roundedRange(value, fallback, minimum, maximum) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? Math.min(maximum, Math.max(minimum, Math.round(numeric))) : fallback;
	}
	function steppedRange(value, fallback, minimum, maximum, step) {
	  const numeric = Number(value), safe = Number.isFinite(numeric) ? numeric : fallback;
	  return Math.min(maximum, Math.max(minimum, Math.round(safe / step) * step));
	}
	function normalizeHexColor(value, fallback = "") {
	  const color = String(value || "").trim().toLowerCase();
	  if (/^#[0-9a-f]{6}$/.test(color)) return color;
	  const fallbackColor = String(fallback || "").trim().toLowerCase();
	  return /^#[0-9a-f]{6}$/.test(fallbackColor) ? fallbackColor : "";
	}
	function normalizeFontFamily(value, fallback) {
	  return FONT_FAMILIES.has(value) ? value : fallback;
	}
	function normalizeCustomFontFamily(value, fallback = "") {
	  return [...String(value ?? fallback).replace(/[\u0000-\u001f\u007f"'`,;{}<>\\]/g, "").replace(/\s+/g, " ").trim()].slice(0, 64).join("");
	}
	function normalizeFontWeight(value, fallback) {
	  const numeric = Number(value);
	  return FONT_WEIGHTS.has(numeric) ? numeric : fallback;
	}
	function normalizeImageProfile(value) {
	  const source = plainRecord(value), sourcePreset = source.preset == null ? IMAGE_PROFILE_DEFAULT.preset : source.preset, preset = sourcePreset === "custom" ? "custom" : IMAGE_SCALE_OPTIONS.includes(Number(sourcePreset)) ? String(Number(sourcePreset)) : "100";
	  return Object.freeze({
	    preset,
	    custom: roundedRange(
	      source.custom,
	      IMAGE_PROFILE_DEFAULT.custom,
	      IMAGE_SCALE_MIN,
	      IMAGE_SCALE_MAX
	    )
	  });
	}
	function normalizeFontProfile(value) {
	  const source = plainRecord(value), family = normalizeFontFamily(source.family, READER_FONT_DEFAULT.family), customFamily = normalizeCustomFontFamily(
	    source.customFamily,
	    READER_FONT_DEFAULT.customFamily
	  ), weight = normalizeFontWeight(source.weight, READER_FONT_DEFAULT.weight);
	  return Object.freeze({
	    family,
	    customFamily,
	    weight,
	    interfaceColor: normalizeHexColor(
	      source.interfaceColor,
	      READER_FONT_DEFAULT.interfaceColor
	    ),
	    interface: roundedRange(
	      source.interface,
	      READER_FONT_DEFAULT.interface,
	      FONT_SCALE_MIN,
	      FONT_SCALE_MAX
	    ),
	    postFamily: normalizeFontFamily(source.postFamily, family),
	    postCustomFamily: normalizeCustomFontFamily(source.postCustomFamily, customFamily),
	    postWeight: normalizeFontWeight(source.postWeight, weight),
	    postColor: normalizeHexColor(source.postColor, READER_FONT_DEFAULT.postColor),
	    post: roundedRange(
	      source.post,
	      READER_FONT_DEFAULT.post,
	      FONT_SCALE_MIN,
	      FONT_SCALE_MAX
	    ),
	    composerFamily: normalizeFontFamily(source.composerFamily, family),
	    composerCustomFamily: normalizeCustomFontFamily(
	      source.composerCustomFamily,
	      customFamily
	    ),
	    composerWeight: normalizeFontWeight(source.composerWeight, weight),
	    composerColor: normalizeHexColor(
	      source.composerColor,
	      READER_FONT_DEFAULT.composerColor
	    ),
	    composer: roundedRange(
	      source.composer,
	      READER_FONT_DEFAULT.composer,
	      FONT_SCALE_MIN,
	      FONT_SCALE_MAX
	    )
	  });
	}
	function normalizeReaderFontProfile(value) {
	  return normalizeFontProfile(value);
	}
	function appearanceColorPair(source, key) {
	  const darkKey = `${key}Dark`, lightFallback = normalizeHexColor(
	    APPEARANCE_PROFILE_DEFAULT[key],
	    APPEARANCE_PROFILE_DEFAULT[key]
	  ), sourceColor = normalizeHexColor(source[key]) || normalizeHexColor(source[darkKey]) || lightFallback;
	  return [sourceColor, sourceColor];
	}
	function normalizeAppearanceProfile(value) {
	  const source = plainRecord(value), [accentColor, accentColorDark] = appearanceColorPair(source, "accentColor"), [linkColor, linkColorDark] = appearanceColorPair(source, "linkColor"), [zebraColor, zebraColorDark] = appearanceColorPair(source, "zebraColor"), [listZebraColor, listZebraColorDark] = appearanceColorPair(
	    source,
	    "listZebraColor"
	  ), [replyLineColor, replyLineColorDark] = appearanceColorPair(
	    source,
	    "replyLineColor"
	  ), [quoteLineColor, quoteLineColorDark] = appearanceColorPair(
	    source,
	    "quoteLineColor"
	  ), [dividerLineColor, dividerLineColorDark] = appearanceColorPair(
	    source,
	    "dividerLineColor"
	  );
	  return Object.freeze({
	    accentColor,
	    accentColorDark,
	    linkColor,
	    linkColorDark,
	    zebraColor,
	    zebraColorDark,
	    zebraRadius: steppedRange(source.zebraRadius, 10, 0, 16, 1),
	    listZebraColor,
	    listZebraColorDark,
	    structureColorsEnabled: typeof source.structureColorsEnabled == "boolean" ? source.structureColorsEnabled : !0,
	    replyLineColor,
	    replyLineColorDark,
	    replyLineWidth: steppedRange(source.replyLineWidth, 1, 0.5, 4, 0.5),
	    replyLineRadius: steppedRange(source.replyLineRadius, 15, 0, 16, 1),
	    quoteLineColor,
	    quoteLineColorDark,
	    quoteLineWidth: steppedRange(source.quoteLineWidth, 0.5, 0.5, 4, 0.5),
	    dividerLineColor,
	    dividerLineColorDark,
	    dividerLineWidth: steppedRange(source.dividerLineWidth, 0.5, 0.5, 4, 0.5)
	  });
	}
	function hexColorToHsl(value) {
	  const color = normalizeHexColor(value);
	  if (!color) return null;
	  const [red, green, blue] = [1, 3, 5].map(
	    (index) => Number.parseInt(color.slice(index, index + 2), 16) / 255
	  ), maximum = Math.max(red, green, blue), minimum = Math.min(red, green, blue), delta = maximum - minimum, lightness = (maximum + minimum) / 2, saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
	  let hue = 0;
	  return delta !== 0 && (maximum === red ? hue = 60 * ((green - blue) / delta % 6) : maximum === green ? hue = 60 * ((blue - red) / delta + 2) : hue = 60 * ((red - green) / delta + 4)), Object.freeze({
	    hue: hue < 0 ? hue + 360 : hue,
	    saturation: saturation * 100,
	    lightness: lightness * 100
	  });
	}
	function hslColorToHex(hue, saturation, lightness) {
	  const normalizedHue = (hue % 360 + 360) % 360, normalizedSaturation = Math.min(100, Math.max(0, saturation)) / 100, normalizedLightness = Math.min(100, Math.max(0, lightness)) / 100, chroma = (1 - Math.abs(2 * normalizedLightness - 1)) * normalizedSaturation, intermediate = chroma * (1 - Math.abs(normalizedHue / 60 % 2 - 1)), offset = normalizedLightness - chroma / 2, sector = Math.floor(normalizedHue / 60);
	  return `#${([
	    [chroma, intermediate, 0],
	    [intermediate, chroma, 0],
	    [0, chroma, intermediate],
	    [0, intermediate, chroma],
	    [intermediate, 0, chroma],
	    [chroma, 0, intermediate]
	  ][sector] ?? [0, 0, 0]).map(
	    (channel) => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")
	  ).join("")}`;
	}
	function normalizeReaderAppearanceProfile(value) {
	  return normalizeAppearanceProfile(value);
	}
	function readerAppearanceEditableProfile(profile) {
	  return Object.freeze(Object.fromEntries(
	    READER_APPEARANCE_SETTING_NAMES.map((name) => [name, profile[name]])
	  ));
	}
	function resolveReaderAppearanceColor(profile, name, theme) {
	  const darkName = `${name}Dark`, defaultColor = String(theme === "dark" ? READER_APPEARANCE_DEFAULT[darkName] : READER_APPEARANCE_DEFAULT[name]), sourceColor = normalizeHexColor(
	    profile[name],
	    READER_APPEARANCE_DEFAULT[name]
	  );
	  if (sourceColor === READER_APPEARANCE_DEFAULT[name]) return defaultColor;
	  const hsl = hexColorToHsl(sourceColor);
	  if (!hsl) return defaultColor;
	  const limit = READER_APPEARANCE_THEME_LIMITS[name], [minimumLightness, maximumLightness] = limit[theme], saturation = Math.min(hsl.saturation, limit.saturationMax), lightness = Math.min(
	    maximumLightness,
	    Math.max(minimumLightness, hsl.lightness)
	  );
	  return Math.abs(saturation - hsl.saturation) < 0.01 && Math.abs(lightness - hsl.lightness) < 0.01 ? sourceColor : hslColorToHex(hsl.hue, saturation, lightness);
	}
	function normalizePerformanceValue(name, value, fallback) {
	  const limit = PERFORMANCE_LIMITS[name], numeric = Number(value);
	  if (!Number.isFinite(numeric)) return fallback;
	  const clamped = Math.min(limit.max, Math.max(limit.min, numeric));
	  return limit.integer ? Math.round(clamped) : Math.round(clamped * 100) / 100;
	}
	function performanceConfigFromInput(input) {
	  const fallback = PERFORMANCE_PRESETS.balanced;
	  return Object.freeze({
	    pageSize: normalizePerformanceValue(
	      "pageSize",
	      input.performancePageSize,
	      fallback.pageSize
	    ),
	    streamOverscanViewports: normalizePerformanceValue(
	      "streamOverscanViewports",
	      input.performanceStreamOverscan,
	      fallback.streamOverscanViewports
	    ),
	    streamMaxItems: normalizePerformanceValue(
	      "streamMaxItems",
	      input.performanceStreamMaxItems,
	      fallback.streamMaxItems
	    ),
	    nestedPrefetchViewports: normalizePerformanceValue(
	      "nestedPrefetchViewports",
	      input.performanceNestedPrefetch,
	      fallback.nestedPrefetchViewports
	    ),
	    requestMaxConcurrent: normalizePerformanceValue(
	      "requestMaxConcurrent",
	      input.performanceRequestConcurrency,
	      fallback.requestMaxConcurrent
	    ),
	    requestMinInterval: normalizePerformanceValue(
	      "requestMinInterval",
	      input.performanceRequestInterval,
	      fallback.requestMinInterval
	    ),
	    requestRateTarget: normalizePerformanceValue(
	      "requestRateTarget",
	      input.performanceRequestRateTarget,
	      fallback.requestRateTarget
	    )
	  });
	}
	function performancePresetForConfig(config) {
	  for (const preset of ["low", "balanced", "high"])
	    if (PERFORMANCE_NAMES.every((name) => config[name] === PERFORMANCE_PRESETS[preset][name]))
	      return preset;
	  return "custom";
	}
	function performancePreferencesPatch(config, preset = performancePresetForConfig(config)) {
	  return {
	    performancePreset: preset,
	    performancePageSize: config.pageSize,
	    performanceStreamOverscan: config.streamOverscanViewports,
	    performanceStreamMaxItems: config.streamMaxItems,
	    performanceNestedPrefetch: config.nestedPrefetchViewports,
	    performanceRequestConcurrency: config.requestMaxConcurrent,
	    performanceRequestInterval: config.requestMinInterval,
	    performanceRequestRateTarget: config.requestRateTarget
	  };
	}
	function readReaderPerformanceConfig(input) {
	  return performanceConfigFromInput(
	    input
	  );
	}
	function createReaderPerformancePreferencesPatch(config, preset) {
	  return performancePreferencesPatch(config, preset);
	}
	function readerPerformancePresetForConfig(config) {
	  return performancePresetForConfig(config);
	}
	function roundLayoutRatio(value) {
	  return Math.round(Number(value) * 100) / 100;
	}
	function layoutTotal(value) {
	  return roundLayoutRatio(LAYOUT_KEYS.reduce((sum, key) => sum + value[key], 0));
	}
	function normalizeLayoutProfile(value, fallback) {
	  const source = plainRecord(value), result = Object.fromEntries(LAYOUT_KEYS.map((key) => {
	    const numeric = Number(source[key]), safe = Number.isFinite(numeric) ? numeric : fallback[key];
	    return [
	      key,
	      roundLayoutRatio(Math.min(100, Math.max(LAYOUT_MIN_RATIOS[key], safe)))
	    ];
	  }));
	  if (layoutTotal(result) > 100) {
	    const extraBudget = 100 - LAYOUT_KEYS.reduce(
	      (sum, key) => sum + LAYOUT_MIN_RATIOS[key],
	      0
	    ), extras = Object.fromEntries(LAYOUT_KEYS.map((key) => [
	      key,
	      Math.max(0, result[key] - LAYOUT_MIN_RATIOS[key])
	    ])), extraTotal = LAYOUT_KEYS.reduce((sum, key) => sum + extras[key], 0) || 1;
	    for (const key of LAYOUT_KEYS)
	      result[key] = roundLayoutRatio(
	        LAYOUT_MIN_RATIOS[key] + extraBudget * extras[key] / extraTotal
	      );
	    let overflow = roundLayoutRatio(layoutTotal(result) - 100);
	    if (overflow > 0) {
	      const correctionKey = LAYOUT_KEYS.find(
	        (key) => result[key] - overflow >= LAYOUT_MIN_RATIOS[key]
	      );
	      correctionKey && (result[correctionKey] = roundLayoutRatio(result[correctionKey] - overflow)), overflow = roundLayoutRatio(layoutTotal(result) - 100), overflow > 0 && (result.main = roundLayoutRatio(result.main - overflow));
	    }
	  }
	  return Object.freeze({ ...result });
	}
	function readerLayoutProfileTotal(profile) {
	  return layoutTotal(profile);
	}
	function readerLayoutRegionMaximum(region) {
	  return roundLayoutRatio(
	    100 - LAYOUT_KEYS.reduce(
	      (total, name) => total + (name === region ? 0 : LAYOUT_MIN_RATIOS[name]),
	      0
	    )
	  );
	}
	function rebalanceReaderLayoutProfile(profile, editedRegion) {
	  const result = { ...profile }, difference = roundLayoutRatio(100 - layoutTotal(result)), direction = difference > 0 ? 1 : -1;
	  let remaining = Math.abs(difference);
	  for (const name of [
	    "main",
	    "left",
	    "right",
	    "gap",
	    "timeline"
	  ]) {
	    if (name === editedRegion || remaining <= 0) continue;
	    const capacity = roundLayoutRatio(
	      direction > 0 ? 100 - result[name] : Math.max(0, result[name] - LAYOUT_MIN_RATIOS[name])
	    ), change = Math.min(capacity, remaining);
	    result[name] = roundLayoutRatio(
	      result[name] + direction * change
	    ), remaining = roundLayoutRatio(remaining - change);
	  }
	  return normalizeLayoutProfile(result, profile);
	}
	function normalizeReaderWindowGroup(source) {
	  const width = Number(source.readerWindowWidth), height = Number(source.readerWindowHeight), x = Number(source.readerWindowX), y = Number(source.readerWindowY);
	  return {
	    readerWindowWidth: Number.isFinite(width) && width > 0 ? Math.max(READER_WINDOW_MIN_WIDTH, Math.round(width)) : 0,
	    readerWindowHeight: Number.isFinite(height) && height > 0 ? Math.max(READER_WINDOW_MIN_HEIGHT, Math.round(height)) : 0,
	    readerWindowX: Number.isFinite(x) && x > 0 ? Math.round(x) : 0,
	    readerWindowY: Number.isFinite(y) && y > 0 ? Math.round(y) : 0,
	    readerWindowLocked: !!source.readerWindowLocked,
	    readerWindowPinned: !!source.readerWindowPinned
	  };
	}
	function normalizeComposerWindowGroup(source) {
	  const result = {};
	  for (const key of [
	    "composerWindowWidth",
	    "composerWindowHeight",
	    "composerWindowX",
	    "composerWindowY"
	  ]) {
	    const value = Number(source[key]);
	    result[key] = Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
	  }
	  return result;
	}
	function normalizeReaderShortcutBinding(value) {
	  const parts = String(value || "").split("+").map((part) => part.trim()).filter(Boolean), code = parts.pop() || "";
	  if (!READER_SHORTCUT_KEYBOARD_CODE.test(code) && !/^Mouse(?:1|3|4|[5-9])$/.test(code) && code !== "Wheel" || /^(?:Control|Alt|Shift|Meta)(?:Left|Right)?$/.test(code))
	    return "";
	  const modifiers = SHORTCUT_MODIFIERS.filter((modifier) => parts.includes(modifier));
	  return parts.some((part) => !SHORTCUT_MODIFIERS.includes(part)) ? "" : [...modifiers, code].join("+");
	}
	function readerShortcutBindingPolicyIssue(value) {
	  const binding = normalizeReaderShortcutBinding(value);
	  if (!binding) return "invalid";
	  if (READER_SHORTCUT_RESERVED_BINDINGS.has(binding)) return "reserved";
	  const parts = binding.split("+"), code = parts.at(-1) ?? "";
	  return parts.length === 1 && /^(?:Key[A-Z]|Digit\d|Numpad\d)$/.test(code) ? "bare-alphanumeric" : null;
	}
	function normalizeReaderShortcutBindings(value) {
	  const source = plainRecord(value), used = /* @__PURE__ */ new Set(), result = {};
	  for (const action of READER_SHORTCUT_ACTIONS) {
	    const selected = Object.hasOwn(source, action) && Array.isArray(source[action]) ? source[action] : READER_SHORTCUT_DEFAULTS[action], bindings = [...new Set(
	      selected.map(normalizeReaderShortcutBinding).filter((binding) => binding && readerShortcutBindingPolicyIssue(binding) === null)
	    )].filter((binding) => used.has(binding) ? !1 : (used.add(binding), !0)).slice(0, 3);
	    result[action] = Object.freeze(bindings);
	  }
	  return Object.freeze(result);
	}
	function normalizeBookmarkTabOrder(value) {
	  const stored = Array.isArray(value) ? [...new Set(value.filter(
	    (type) => BOOKMARK_TAB_TYPES.includes(type)
	  ))] : [];
	  return Object.freeze([
	    ...stored,
	    ...BOOKMARK_TAB_TYPES.filter((type) => !stored.includes(type))
	  ]);
	}
	function normalizeJumpValue(name, value) {
	  const limit = JUMP_HIGHLIGHT_LIMITS[name], fallback = JUMP_HIGHLIGHT_DEFAULTS[name], numeric = Number(value);
	  if (!Number.isFinite(numeric)) return fallback;
	  const clamped = Math.min(limit.max, Math.max(limit.min, numeric));
	  return limit.integer ? Math.round(clamped) : Math.round(Math.round(clamped / limit.step) * limit.step * 100) / 100;
	}
	function normalizeBoostPreferences(source) {
	  const normalized = (0, import_reader_boost_copy_settings.normalizeBoostCopySettings)({
	    mode: source.boostCopyMode === "text" ? "text" : "counter",
	    prefix: String(source.boostCopyPrefix ?? ""),
	    counterMarker: String(source.boostCopyCounterMarker ?? ""),
	    counterStep: Number(source.boostCopyCounterStep),
	    fixedSuffix: String(source.boostCopyFixedSuffix ?? "")
	  });
	  return {
	    boostCopyMode: normalized.mode,
	    boostCopyPrefix: normalized.prefix,
	    boostCopyCounterMarker: normalized.counterMarker,
	    boostCopyCounterStep: normalized.counterStep,
	    boostCopyFixedSuffix: normalized.fixedSuffix
	  };
	}
	function normalizeEnvironment(environment) {
	  return Object.freeze({
	    viewportWidth: finiteViewport(environment.viewportWidth, "viewportWidth"),
	    viewportHeight: finiteViewport(environment.viewportHeight, "viewportHeight")
	  });
	}
	function createReaderPreferencesDefaults(environment) {
	  const viewport = normalizeEnvironment(environment), performance = performancePreferencesPatch(
	    PERFORMANCE_PRESETS.balanced,
	    "balanced"
	  );
	  return Object.freeze({
	    topicReaderMode: "fullpage",
	    imageProfile: IMAGE_PROFILE_DEFAULT,
	    imageProfilesShared: !0,
	    floatingImageProfile: IMAGE_PROFILE_DEFAULT,
	    fullpageImageProfile: IMAGE_PROFILE_DEFAULT,
	    mobileImageProfile: IMAGE_PROFILE_DEFAULT,
	    lightboxOriginalByDefault: !0,
	    lightboxCommentsExpandedByDefault: !0,
	    lightboxDescriptionExpanded: !1,
	    lightboxDescriptionHeight: LIGHTBOX_DESCRIPTION_HEIGHT_DEFAULT,
	    lightboxCommentsWidthPercent: LIGHTBOX_COMMENTS_WIDTH_DEFAULT,
	    themeMode: "system",
	    autoDarkModeEnabled: !1,
	    autoDarkModeStartTime: "sunset",
	    fontRenderingEnabled: !0,
	    fontRenderingOnHost: !0,
	    hostFontFamily: "system",
	    hostFontCustomFamily: "",
	    hostFontWeight: 400,
	    hostFontColor: "",
	    hostEmbeddedTitleScale: READER_HOST_FONT_SCALE_DEFAULTS.title,
	    hostEmbeddedAvatarScale: READER_HOST_FONT_SCALE_DEFAULTS.avatar,
	    hostEmbeddedStatsScale: READER_HOST_FONT_SCALE_DEFAULTS.stats,
	    hostEmbeddedLabelCardScale: READER_HOST_FONT_SCALE_DEFAULTS.labelCard,
	    fontProfile: READER_FONT_DEFAULT,
	    appearanceProfile: APPEARANCE_PROFILE_DEFAULT,
	    ...performance,
	    layoutProfile: READER_LAYOUT_DEFAULT,
	    fullpageLayoutProfile: READER_FULLPAGE_LAYOUT_DEFAULT,
	    readerWindowWidth: 0,
	    readerWindowHeight: 0,
	    readerWindowX: 0,
	    readerWindowY: 0,
	    readerWindowLocked: !1,
	    readerWindowPinned: !1,
	    listReaderMode: "embed-right",
	    listReaderEmbedWidth: Math.round(viewport.viewportWidth * 0.45),
	    composerWindowWidth: 0,
	    composerWindowHeight: 0,
	    composerWindowX: 0,
	    composerWindowY: 0,
	    historySortMode: "recent-viewed",
	    bookmarkTabOrder: BOOKMARK_TAB_TYPES,
	    historyButtonsAlwaysVisible: !0,
	    readerQueueAlwaysVisibleWhenEmpty: !0,
	    historyEdgeTriggerPercent: HISTORY_EDGE_TRIGGER_DEFAULT,
	    loadingAnimation: "quoteecho",
	    translationMode: "original",
	    translationTheme: import_reader_translation_presentation.DEFAULT_READER_TRANSLATION_THEME,
	    openTopicsAtFirstPost: !0,
	    doubleEscapeToCloseReader: !1,
	    confirmNativeComposerClose: !1,
	    readerShortcutBindings: normalizeReaderShortcutBindings(READER_SHORTCUT_DEFAULTS),
	    topicActionRailVisible: !0,
	    topicActionRailFixed: !1,
	    topicActionRailMode: "compact",
	    topicActionRailPositions: Object.freeze({
	      floating: Object.freeze({ x: "left", y: 0.95 }),
	      fullpage: Object.freeze({ x: "left", y: 0.95 }),
	      embedded: Object.freeze({ x: "left", y: 0.95 })
	    }),
	    unwantedTopicFilterEnabled: !1,
	    unwantedTopicFilterCategories: Object.freeze([]),
	    unwantedTopicFilterLabels: Object.freeze([]),
	    unwantedTopicFilterTopicAuthors: Object.freeze([]),
	    unwantedTopicFilterTopicFields: Object.freeze([]),
	    unwantedTopicFilterPostAuthors: Object.freeze([]),
	    expandNestedRepliesByDefault: !0,
	    expandLeafNestedReplies: !1,
	    aggregateDescendantReplies: !0,
	    inlineReplyTreeMaxDepth: INLINE_REPLY_TREE_DEFAULT_DEPTH,
	    hideNestedReplyFloors: !0,
	    jumpHighlightColor: JUMP_HIGHLIGHT_DEFAULTS.color,
	    jumpHighlightRadius: JUMP_HIGHLIGHT_DEFAULTS.radius,
	    jumpHighlightBorderWidth: JUMP_HIGHLIGHT_DEFAULTS.borderWidth,
	    jumpHighlightRate: JUMP_HIGHLIGHT_DEFAULTS.rate,
	    jumpHighlightCount: JUMP_HIGHLIGHT_DEFAULTS.count,
	    boostCopyMode: import_reader_boost_copy_settings.DEFAULT_BOOST_COPY_SETTINGS.mode,
	    boostCopyPrefix: import_reader_boost_copy_settings.DEFAULT_BOOST_COPY_SETTINGS.prefix,
	    boostCopyCounterMarker: import_reader_boost_copy_settings.DEFAULT_BOOST_COPY_SETTINGS.counterMarker,
	    boostCopyCounterStep: import_reader_boost_copy_settings.DEFAULT_BOOST_COPY_SETTINGS.counterStep,
	    boostCopyFixedSuffix: import_reader_boost_copy_settings.DEFAULT_BOOST_COPY_SETTINGS.fixedSuffix
	  });
	}
	function prepareStoredReaderPreferences(value) {
	  const source = { ...plainRecord(value) }, preset = source.performancePreset;
	  if (preset === "low" || preset === "balanced" || preset === "high") {
	    const patch = performancePreferencesPatch(PERFORMANCE_PRESETS[preset], preset);
	    return Object.freeze({ ...source, ...patch });
	  }
	  return Object.freeze(source);
	}
	function normalizeReaderPreferences(value, environment) {
	  const viewport = normalizeEnvironment(environment), defaults = createReaderPreferencesDefaults(viewport), known = Object.fromEntries(
	    Object.keys(defaults).filter((key) => Object.hasOwn(value, key)).map((key) => [key, value[key]])
	  ), source = { ...defaults, ...known }, performance = performancePreferencesPatch(performanceConfigFromInput(source)), expandLeafNestedReplies = source.expandLeafNestedReplies === !0, aggregateDescendantReplies = source.aggregateDescendantReplies === !0, expandNestedRepliesByDefault = aggregateDescendantReplies || source.expandNestedRepliesByDefault !== !1 || !expandLeafNestedReplies, windowPreferences = normalizeReaderWindowGroup(source), composerPreferences = normalizeComposerWindowGroup(source), boostPreferences = normalizeBoostPreferences(source), lightboxMaximum = Math.max(
	    LIGHTBOX_DESCRIPTION_HEIGHT_MIN,
	    Math.floor(viewport.viewportHeight * 0.4)
	  ), lightboxHeight = Math.round(Number(source.lightboxDescriptionHeight)), listWidth = Number(source.listReaderEmbedWidth), historyEdge = Number(source.historyEdgeTriggerPercent), depth = Number.parseInt(String(source.inlineReplyTreeMaxDepth), 10), loadingAnimation = source.loadingAnimation === "random" || READER_LOADING_ANIMATIONS.has(source.loadingAnimation) ? source.loadingAnimation : "quoteecho", topicReaderMode = source.topicReaderMode === "floating" ? "floating" : "fullpage", imageProfile = normalizeImageProfile(source.imageProfile), imageProfilesShared = source.imageProfilesShared !== !1, floatingImageProfile = imageProfilesShared ? imageProfile : normalizeImageProfile(
	    source.floatingImageProfile ?? imageProfile
	  ), fullpageImageProfile = imageProfilesShared ? imageProfile : normalizeImageProfile(
	    source.fullpageImageProfile ?? imageProfile
	  ), mobileImageProfile = imageProfilesShared ? imageProfile : normalizeImageProfile(
	    source.mobileImageProfile ?? imageProfile
	  ), listReaderMode = [
	    "floating",
	    "fullpage",
	    "embed-left",
	    "embed-right"
	  ].includes(String(source.listReaderMode)) ? source.listReaderMode : defaults.listReaderMode, themeMode = ["light", "dark", "system"].includes(String(source.themeMode)) ? source.themeMode : "system", autoDarkModeStartTime = /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(
	    String(source.autoDarkModeStartTime)
	  ) ? String(source.autoDarkModeStartTime) : "sunset", translationMode = ["bilingual", "translation"].includes(
	    String(source.translationMode)
	  ) ? source.translationMode : "original", translationTheme = (0, import_reader_translation_presentation.normalizeReaderTranslationTheme)(
	    source.translationTheme
	  ), jumpColor = String(source.jumpHighlightColor || "").trim().toLowerCase(), unwantedTopicFilter = (0, import_reader_unwanted_topic_filter.normalizeReaderUnwantedTopicFilterPreferences)({
	    enabled: source.unwantedTopicFilterEnabled === !0,
	    categories: source.unwantedTopicFilterCategories,
	    labels: source.unwantedTopicFilterLabels,
	    topicAuthors: source.unwantedTopicFilterTopicAuthors,
	    topicFields: source.unwantedTopicFilterTopicFields,
	    postAuthors: source.unwantedTopicFilterPostAuthors
	  });
	  return Object.freeze({
	    topicReaderMode,
	    imageProfile,
	    imageProfilesShared,
	    floatingImageProfile,
	    fullpageImageProfile,
	    mobileImageProfile,
	    lightboxOriginalByDefault: source.lightboxOriginalByDefault === !0,
	    lightboxCommentsExpandedByDefault: source.lightboxCommentsExpandedByDefault === !0,
	    lightboxDescriptionExpanded: source.lightboxDescriptionExpanded === !0,
	    lightboxDescriptionHeight: Math.min(
	      lightboxMaximum,
	      Math.max(
	        LIGHTBOX_DESCRIPTION_HEIGHT_MIN,
	        Number.isFinite(lightboxHeight) ? lightboxHeight : LIGHTBOX_DESCRIPTION_HEIGHT_DEFAULT
	      )
	    ),
	    lightboxCommentsWidthPercent: Math.min(
	      LIGHTBOX_COMMENTS_WIDTH_MAX,
	      Math.max(
	        LIGHTBOX_COMMENTS_WIDTH_MIN,
	        Number.isFinite(Number(source.lightboxCommentsWidthPercent)) ? Number(source.lightboxCommentsWidthPercent) : LIGHTBOX_COMMENTS_WIDTH_DEFAULT
	      )
	    ),
	    themeMode,
	    autoDarkModeEnabled: source.autoDarkModeEnabled === !0,
	    autoDarkModeStartTime,
	    fontRenderingEnabled: source.fontRenderingEnabled !== !1,
	    fontRenderingOnHost: source.fontRenderingOnHost === !0,
	    hostFontFamily: normalizeFontFamily(source.hostFontFamily, "system"),
	    hostFontCustomFamily: normalizeCustomFontFamily(source.hostFontCustomFamily),
	    hostFontWeight: normalizeFontWeight(source.hostFontWeight, 400),
	    hostFontColor: normalizeHexColor(source.hostFontColor),
	    hostEmbeddedTitleScale: roundedRange(
	      source.hostEmbeddedTitleScale,
	      READER_HOST_FONT_SCALE_DEFAULTS.title,
	      HOST_EMBED_SIZE_MIN,
	      HOST_EMBED_SIZE_MAX
	    ),
	    hostEmbeddedAvatarScale: roundedRange(
	      source.hostEmbeddedAvatarScale,
	      READER_HOST_FONT_SCALE_DEFAULTS.avatar,
	      HOST_EMBED_SIZE_MIN,
	      HOST_EMBED_SIZE_MAX
	    ),
	    hostEmbeddedStatsScale: roundedRange(
	      source.hostEmbeddedStatsScale,
	      READER_HOST_FONT_SCALE_DEFAULTS.stats,
	      HOST_EMBED_SIZE_MIN,
	      HOST_EMBED_SIZE_MAX
	    ),
	    hostEmbeddedLabelCardScale: roundedRange(
	      source.hostEmbeddedLabelCardScale,
	      READER_HOST_FONT_SCALE_DEFAULTS.labelCard,
	      HOST_EMBED_SIZE_MIN,
	      HOST_EMBED_SIZE_MAX
	    ),
	    fontProfile: normalizeFontProfile(source.fontProfile),
	    appearanceProfile: normalizeAppearanceProfile(source.appearanceProfile),
	    ...performance,
	    layoutProfile: normalizeLayoutProfile(source.layoutProfile, READER_LAYOUT_DEFAULT),
	    fullpageLayoutProfile: normalizeLayoutProfile(
	      source.fullpageLayoutProfile,
	      READER_FULLPAGE_LAYOUT_DEFAULT
	    ),
	    ...windowPreferences,
	    listReaderMode,
	    listReaderEmbedWidth: Number.isFinite(listWidth) ? Math.max(READER_EMBED_MIN_WIDTH, Math.round(listWidth)) : defaults.listReaderEmbedWidth,
	    ...composerPreferences,
	    historySortMode: source.historySortMode === "first-viewed" ? "first-viewed" : "recent-viewed",
	    bookmarkTabOrder: normalizeBookmarkTabOrder(source.bookmarkTabOrder),
	    historyButtonsAlwaysVisible: source.historyButtonsAlwaysVisible === !0,
	    readerQueueAlwaysVisibleWhenEmpty: source.readerQueueAlwaysVisibleWhenEmpty !== !1,
	    historyEdgeTriggerPercent: Number.isFinite(historyEdge) ? Math.min(
	      HISTORY_EDGE_TRIGGER_MAX,
	      Math.max(HISTORY_EDGE_TRIGGER_MIN, Math.round(historyEdge))
	    ) : HISTORY_EDGE_TRIGGER_DEFAULT,
	    loadingAnimation,
	    translationMode,
	    translationTheme,
	    openTopicsAtFirstPost: source.openTopicsAtFirstPost !== !1,
	    doubleEscapeToCloseReader: source.doubleEscapeToCloseReader === !0,
	    confirmNativeComposerClose: source.confirmNativeComposerClose === !0,
	    readerShortcutBindings: normalizeReaderShortcutBindings(source.readerShortcutBindings),
	    topicActionRailVisible: source.topicActionRailVisible !== !1,
	    topicActionRailFixed: source.topicActionRailFixed === !0,
	    topicActionRailMode: source.topicActionRailMode === "collapsed" ? "collapsed" : "compact",
	    topicActionRailPositions: normalizeTopicActionRailPositions(
	      Object.hasOwn(value, "topicActionRailPositions") ? source.topicActionRailPositions : null,
	      value.topicActionRailPosition
	    ),
	    unwantedTopicFilterEnabled: unwantedTopicFilter.enabled,
	    unwantedTopicFilterCategories: unwantedTopicFilter.categories,
	    unwantedTopicFilterLabels: unwantedTopicFilter.labels,
	    unwantedTopicFilterTopicAuthors: unwantedTopicFilter.topicAuthors,
	    unwantedTopicFilterTopicFields: unwantedTopicFilter.topicFields,
	    unwantedTopicFilterPostAuthors: unwantedTopicFilter.postAuthors,
	    expandNestedRepliesByDefault,
	    expandLeafNestedReplies,
	    aggregateDescendantReplies,
	    inlineReplyTreeMaxDepth: Math.min(
	      INLINE_REPLY_TREE_MAX_DEPTH,
	      Math.max(1, depth || INLINE_REPLY_TREE_DEFAULT_DEPTH)
	    ),
	    hideNestedReplyFloors: source.hideNestedReplyFloors === !0,
	    jumpHighlightColor: /^#[0-9a-f]{6}$/.test(jumpColor) ? jumpColor : JUMP_HIGHLIGHT_DEFAULTS.color,
	    jumpHighlightRadius: normalizeJumpValue("radius", source.jumpHighlightRadius),
	    jumpHighlightBorderWidth: normalizeJumpValue(
	      "borderWidth",
	      source.jumpHighlightBorderWidth
	    ),
	    jumpHighlightRate: normalizeJumpValue("rate", source.jumpHighlightRate),
	    jumpHighlightCount: normalizeJumpValue("count", source.jumpHighlightCount),
	    ...boostPreferences
	  });
	}
	function createReaderPreferencesConfigCodec(options) {
	  const defaults = createReaderPreferencesDefaults(options.environment), normalize = (value) => normalizeReaderPreferences(value, options.environment), unwantedTopicFilterDefaults = Object.freeze({
	    unwantedTopicFilterEnabled: !1,
	    unwantedTopicFilterCategories: Object.freeze([]),
	    unwantedTopicFilterLabels: Object.freeze([]),
	    unwantedTopicFilterTopicAuthors: Object.freeze([]),
	    unwantedTopicFilterTopicFields: Object.freeze([]),
	    unwantedTopicFilterPostAuthors: Object.freeze([])
	  });
	  return new import_preferences_config_codec.PreferencesConfigCodec({
	    format: READER_CONFIG_EXPORT_FORMAT,
	    schemaVersion: READER_CONFIG_EXPORT_VERSION,
	    scriptVersion: options.scriptVersion,
	    defaults,
	    normalize,
	    legacyImportRules: [
	      {
	        missingDefaults: unwantedTopicFilterDefaults
	      },
	      {
	        missingDefaults: {
	          ...unwantedTopicFilterDefaults,
	          autoDarkModeEnabled: !1,
	          autoDarkModeStartTime: "sunset"
	        }
	      },
	      {
	        missingDefaults: {
	          ...unwantedTopicFilterDefaults,
	          autoDarkModeEnabled: !1,
	          autoDarkModeStartTime: "sunset",
	          fullpageLayoutProfile: READER_FULLPAGE_LAYOUT_DEFAULT
	        }
	      },
	      {
	        missingDefaults: {
	          ...unwantedTopicFilterDefaults,
	          autoDarkModeEnabled: !1,
	          autoDarkModeStartTime: "sunset",
	          fullpageLayoutProfile: READER_FULLPAGE_LAYOUT_DEFAULT,
	          confirmNativeComposerClose: !0
	        }
	      },
	      {
	        missingDefaults: {
	          ...unwantedTopicFilterDefaults,
	          autoDarkModeEnabled: !1,
	          autoDarkModeStartTime: "sunset",
	          readerShortcutBindings: defaults.readerShortcutBindings,
	          topicActionRailMode: "compact",
	          inlineReplyTreeMaxDepth: 1
	        }
	      }
	    ],
	    ...options.now ? { now: options.now } : {}
	  });
	}
	function createReaderPreferencesRepository(options) {
	  const defaults = createReaderPreferencesDefaults(options.environment);
	  return new import_preferences_repository.PreferencesRepository({
	    key: READER_PREFERENCES_STORAGE_KEY,
	    storage: options.storage,
	    defaults,
	    normalize: (value) => normalizeReaderPreferences(value, options.environment),
	    prepareStored: prepareStoredReaderPreferences
	  });
	}
}, "912c4eddb46b49e2508257294d02732ccf3c2b68e66442d6e7143837ea08df20");

/* Source: lite/src/state/reader-settings-config-manager.ts */
runtime.register("src/state/reader-settings-config-manager.js", function(module, exports, require) {
	var reader_settings_config_manager_exports = {};
	__export(reader_settings_config_manager_exports, {
	  READER_SETTINGS_CONFIG_EXPORT_VERSION: () => READER_SETTINGS_CONFIG_EXPORT_VERSION,
	  READER_SETTINGS_CONFIG_OMITTED_SECRETS: () => READER_SETTINGS_CONFIG_OMITTED_SECRETS,
	  ReaderSettingsConfigCodec: () => ReaderSettingsConfigCodec,
	  ReaderSettingsConfigManager: () => ReaderSettingsConfigManager
	});
	module.exports = __toCommonJS(reader_settings_config_manager_exports);
	var import_reader_custom_site_repository = require("../site/reader-custom-site-repository.js"), import_reader_webdav_model = require("../sync/reader-webdav-model.js"), import_reader_translation_config = require("../translation/reader-translation-config.js"), import_reader_preferences_schema = require("./reader-preferences-schema.js");
	const READER_SETTINGS_CONFIG_EXPORT_VERSION = 9, READER_SETTINGS_CONFIG_LEGACY_PORTABLE_VERSIONS = Object.freeze([6, 7, 8]), READER_SETTINGS_CONFIG_OMITTED_SECRETS = Object.freeze([
	  "translation.apiKey",
	  "webDav.username",
	  "webDav.password"
	]);
	function invalidConfig(cause) {
	  return cause === void 0 ? new Error("invalid_config") : new Error("invalid_config", { cause });
	}
	function record(value) {
	  if (!value || typeof value != "object" || Array.isArray(value))
	    throw invalidConfig();
	  return value;
	}
	function exactKeys(value, expected) {
	  const actual = Object.keys(value).sort(), canonical = [...expected].sort();
	  if (actual.length !== canonical.length || actual.some((key, index) => key !== canonical[index])) throw invalidConfig();
	}
	function portableTranslationProfile(profile) {
	  return Object.freeze({
	    baseUrl: profile.baseUrl,
	    models: profile.models,
	    modelCatalog: profile.modelCatalog,
	    model: profile.model,
	    prompt: profile.prompt,
	    temperature: profile.temperature,
	    reasoningEffort: profile.reasoningEffort,
	    requestsPerMinute: profile.requestsPerMinute,
	    tokensPerMinute: profile.tokensPerMinute,
	    animation: profile.animation
	  });
	}
	function portableTranslationConfig(value) {
	  const normalized = (0, import_reader_translation_config.normalizeReaderTranslationConfig)(value);
	  return Object.freeze({
	    profiles: Object.freeze(normalized.profiles.map(
	      portableTranslationProfile
	    )),
	    activeBaseUrl: normalized.activeBaseUrl
	  });
	}
	function parsePortableTranslationConfig(value, sourceVersion = READER_SETTINGS_CONFIG_EXPORT_VERSION) {
	  if (value === null) return null;
	  const source = record(value);
	  if (exactKeys(source, ["profiles", "activeBaseUrl"]), !Array.isArray(source.profiles) || !source.profiles.length)
	    throw invalidConfig();
	  const profiles = source.profiles.map((value2) => {
	    const profile = record(value2), hasModels = sourceVersion >= 8, hasModelCatalog = sourceVersion >= 9;
	    exactKeys(profile, [
	      "baseUrl",
	      ...hasModels ? ["models"] : [],
	      ...hasModelCatalog ? ["modelCatalog"] : [],
	      "model",
	      "prompt",
	      "temperature",
	      "reasoningEffort",
	      "requestsPerMinute",
	      "tokensPerMinute",
	      "animation"
	    ]);
	    const parsed = (0, import_reader_translation_config.normalizeReaderTranslationConfig)({
	      profiles: [{ ...profile, apiKey: "" }],
	      activeBaseUrl: profile.baseUrl
	    }).profiles[0];
	    if (!parsed) throw invalidConfig();
	    const portable = portableTranslationProfile(parsed);
	    if (Object.entries(profile).some(([key, sourceValue]) => key !== "models" && key !== "modelCatalog" && sourceValue !== portable[key]) || hasModels && JSON.stringify(profile.models) !== JSON.stringify(portable.models) || hasModelCatalog && JSON.stringify(profile.modelCatalog) !== JSON.stringify(portable.modelCatalog))
	      throw invalidConfig();
	    return parsed;
	  });
	  if (new Set(profiles.map((profile) => profile.baseUrl)).size !== profiles.length)
	    throw invalidConfig();
	  const activeBaseUrl = (0, import_reader_translation_config.normalizeReaderTranslationBaseUrl)(source.activeBaseUrl);
	  if (activeBaseUrl !== source.activeBaseUrl || !profiles.some((profile) => profile.baseUrl === activeBaseUrl)) throw invalidConfig();
	  return (0, import_reader_translation_config.normalizeReaderTranslationConfig)({
	    profiles: Object.freeze(profiles),
	    activeBaseUrl
	  });
	}
	function portableWebDavConfig(value) {
	  const normalized = (0, import_reader_webdav_model.normalizeReaderWebDavConfig)(value);
	  return Object.freeze({
	    endpoint: normalized.endpoint,
	    remotePath: normalized.remotePath,
	    categories: Object.freeze({ ...normalized.categories }),
	    autoSyncEnabled: normalized.autoSyncEnabled,
	    autoSyncIntervalMinutes: normalized.autoSyncIntervalMinutes
	  });
	}
	function parsePortableWebDavConfig(value, sourceVersion = READER_SETTINGS_CONFIG_EXPORT_VERSION) {
	  if (value === null) return null;
	  const source = record(value);
	  exactKeys(source, [
	    "endpoint",
	    "remotePath",
	    "categories",
	    "autoSyncEnabled",
	    "autoSyncIntervalMinutes"
	  ]);
	  const categories = record(source.categories), historyCategories = /* @__PURE__ */ new Set([
	    "notification-history",
	    "activity-history"
	  ]), missesBothHistoryCategories = [...historyCategories].every(
	    (category) => !Object.hasOwn(categories, category)
	  ), expectedCategories = sourceVersion === 6 ? import_reader_webdav_model.READER_WEBDAV_CATEGORIES.filter((category) => category !== "offline-topics" && !historyCategories.has(category)) : sourceVersion < READER_SETTINGS_CONFIG_EXPORT_VERSION && missesBothHistoryCategories ? import_reader_webdav_model.READER_WEBDAV_CATEGORIES.filter((category) => !historyCategories.has(category)) : import_reader_webdav_model.READER_WEBDAV_CATEGORIES;
	  if (exactKeys(categories, expectedCategories), Object.values(categories).some((selected) => typeof selected != "boolean"))
	    throw invalidConfig();
	  const normalizedCategories = (0, import_reader_webdav_model.createReaderWebDavCategorySelection)(categories), normalized = (0, import_reader_webdav_model.normalizeReaderWebDavConfig)({
	    ...source,
	    categories: normalizedCategories,
	    username: "",
	    password: ""
	  });
	  if ((0, import_reader_webdav_model.validateReaderWebDavConfig)(normalized, {
	    requireCredentials: !1
	  }).length) throw invalidConfig();
	  const portable = portableWebDavConfig(normalized);
	  if (portable.endpoint !== source.endpoint || portable.remotePath !== source.remotePath || portable.autoSyncEnabled !== source.autoSyncEnabled || portable.autoSyncIntervalMinutes !== source.autoSyncIntervalMinutes || import_reader_webdav_model.READER_WEBDAV_CATEGORIES.some((category) => portable.categories[category] !== normalizedCategories[category])) throw invalidConfig();
	  return normalized;
	}
	function parseCustomSites(value) {
	  if (!Array.isArray(value)) throw invalidConfig();
	  const sites = value.map((entry) => {
	    if (typeof entry != "string") throw invalidConfig();
	    const normalized = (0, import_reader_custom_site_repository.normalizeReaderCustomSiteHost)(entry);
	    if (!normalized || normalized !== entry || (0, import_reader_custom_site_repository.readerBuiltinDiscourseHost)(normalized)) throw invalidConfig();
	    return normalized;
	  });
	  if (new Set(sites).size !== sites.length) throw invalidConfig();
	  return Object.freeze([...sites].sort());
	}
	class ReaderSettingsConfigCodec {
	  #preferences;
	  constructor(preferences) {
	    this.#preferences = preferences;
	  }
	  export(input) {
	    const preferences = this.#preferences.export(input.preferences);
	    return Object.freeze({
	      ...preferences,
	      schemaVersion: READER_SETTINGS_CONFIG_EXPORT_VERSION,
	      customSites: parseCustomSites(input.customSites),
	      translation: input.translation ? portableTranslationConfig(input.translation) : null,
	      webDav: input.webDav ? portableWebDavConfig(input.webDav) : null,
	      omittedSecrets: READER_SETTINGS_CONFIG_OMITTED_SECRETS
	    });
	  }
	  import(payload) {
	    try {
	      return this.#import(payload);
	    } catch (cause) {
	      throw cause instanceof Error && cause.message === "invalid_config" ? cause : invalidConfig(cause);
	    }
	  }
	  #import(payload) {
	    const source = record(payload), schemaVersion = source.schemaVersion;
	    if (typeof schemaVersion != "number" || !Number.isSafeInteger(schemaVersion)) throw invalidConfig();
	    if (schemaVersion === import_reader_preferences_schema.READER_CONFIG_EXPORT_VERSION) {
	      const preferences2 = this.#preferences.import(payload);
	      return Object.freeze({
	        sourceVersion: schemaVersion,
	        settingsCount: source.settingsCount,
	        preferences: preferences2,
	        includesPortableSections: !1,
	        customSites: null,
	        translation: null,
	        webDav: null
	      });
	    }
	    if (schemaVersion !== READER_SETTINGS_CONFIG_EXPORT_VERSION && !READER_SETTINGS_CONFIG_LEGACY_PORTABLE_VERSIONS.includes(schemaVersion) || (exactKeys(source, [
	      "format",
	      "schemaVersion",
	      "scriptVersion",
	      "exportedAt",
	      "settingsCount",
	      "settings",
	      "customSites",
	      "translation",
	      "webDav",
	      "omittedSecrets"
	    ]), source.format !== import_reader_preferences_schema.READER_CONFIG_EXPORT_FORMAT || !Array.isArray(source.omittedSecrets) || source.omittedSecrets.length !== READER_SETTINGS_CONFIG_OMITTED_SECRETS.length || source.omittedSecrets.some((key, index) => key !== READER_SETTINGS_CONFIG_OMITTED_SECRETS[index]))) throw invalidConfig();
	    const preferences = this.#preferences.import({
	      format: source.format,
	      schemaVersion: import_reader_preferences_schema.READER_CONFIG_EXPORT_VERSION,
	      scriptVersion: source.scriptVersion,
	      exportedAt: source.exportedAt,
	      settingsCount: source.settingsCount,
	      settings: source.settings
	    });
	    return Object.freeze({
	      sourceVersion: schemaVersion,
	      settingsCount: source.settingsCount,
	      preferences,
	      includesPortableSections: !0,
	      customSites: parseCustomSites(source.customSites),
	      translation: parsePortableTranslationConfig(
	        source.translation,
	        schemaVersion
	      ),
	      webDav: parsePortableWebDavConfig(
	        source.webDav,
	        schemaVersion
	      )
	    });
	  }
	}
	class ReaderSettingsConfigManager {
	  #codec;
	  #defaults;
	  #preferences;
	  #customSites;
	  #translation;
	  #webDav;
	  constructor(options) {
	    this.#codec = options.codec, this.#defaults = options.defaults, this.#preferences = options.preferences, this.#customSites = options.customSites, this.#translation = options.translation, this.#webDav = options.webDav;
	  }
	  async export() {
	    return await Promise.all([
	      this.#customSites.load(),
	      this.#translation?.load(),
	      this.#webDav?.load()
	    ]), this.#codec.export({
	      preferences: this.#preferences.read(),
	      customSites: this.#customSites.snapshot,
	      translation: this.#translation?.snapshot.config ?? null,
	      webDav: this.#webDav?.snapshot.config ?? null
	    });
	  }
	  prepare(payload) {
	    return this.#codec.import(payload);
	  }
	  apply(prepared) {
	    return this.#apply(prepared, !0);
	  }
	  reset() {
	    return this.#apply(Object.freeze({
	      sourceVersion: READER_SETTINGS_CONFIG_EXPORT_VERSION,
	      settingsCount: Object.keys(this.#defaults).length,
	      preferences: this.#defaults,
	      includesPortableSections: !0,
	      customSites: Object.freeze([]),
	      translation: (0, import_reader_translation_config.createReaderTranslationDefaultConfig)(),
	      webDav: (0, import_reader_webdav_model.createReaderWebDavDefaultConfig)()
	    }), !1);
	  }
	  async #apply(prepared, preserveLocalSecrets) {
	    const rollbacks = [], skippedSections = [];
	    let customSitesApplied = !1, translationApplied = !1, webDavApplied = !1, preservedTranslationApiKeys = 0, preservedWebDavCredentials = !1, webDavAutoSyncDisabled = !1;
	    try {
	      if (prepared.includesPortableSections && prepared.customSites)
	        if (this.#customSites.writable) {
	          const previous = await this.#customSites.load();
	          await this.#customSites.replaceExternal(prepared.customSites), rollbacks.unshift(async () => {
	            await this.#customSites.replaceExternal(previous);
	          }), customSitesApplied = !0;
	        } else
	          skippedSections.push("customSites");
	      if (prepared.includesPortableSections && prepared.translation)
	        if (this.#translation) {
	          const previous = (await this.#translation.load()).config, apiKeys = new Map(previous.profiles.map((profile) => [
	            profile.baseUrl,
	            profile.apiKey
	          ])), next = Object.freeze({
	            ...prepared.translation,
	            profiles: Object.freeze(prepared.translation.profiles.map(
	              (profile) => {
	                const apiKey = preserveLocalSecrets ? apiKeys.get(profile.baseUrl) ?? "" : "";
	                return apiKey && (preservedTranslationApiKeys += 1), Object.freeze({ ...profile, apiKey });
	              }
	            ))
	          });
	          await this.#translation.saveConfig(next), rollbacks.unshift(async () => {
	            await this.#translation.saveConfig(previous);
	          }), translationApplied = !0;
	        } else
	          skippedSections.push("translation");
	      if (prepared.includesPortableSections && prepared.webDav)
	        if (this.#webDav) {
	          const previous = (await this.#webDav.load()).config, sameEndpoint = previous.endpoint === prepared.webDav.endpoint, username = preserveLocalSecrets && sameEndpoint ? previous.username : "", password = preserveLocalSecrets && sameEndpoint ? previous.password : "";
	          preservedWebDavCredentials = !!(username && password), webDavAutoSyncDisabled = prepared.webDav.autoSyncEnabled && !preservedWebDavCredentials;
	          const next = Object.freeze({
	            ...prepared.webDav,
	            username,
	            password,
	            autoSyncEnabled: prepared.webDav.autoSyncEnabled && preservedWebDavCredentials
	          });
	          await this.#webDav.saveConfig(next), rollbacks.unshift(async () => {
	            await this.#webDav.saveConfig(previous);
	          }), webDavApplied = !0;
	        } else
	          skippedSections.push("webDav");
	      const previousPreferences = this.#preferences.read();
	      return await this.#preferences.update(prepared.preferences), rollbacks.unshift(async () => {
	        await this.#preferences.update(previousPreferences);
	      }), Object.freeze({
	        sourceVersion: prepared.sourceVersion,
	        settingsCount: prepared.settingsCount,
	        customSitesApplied,
	        translationApplied,
	        webDavApplied,
	        preservedTranslationApiKeys,
	        preservedWebDavCredentials,
	        webDavAutoSyncDisabled,
	        skippedSections: Object.freeze(skippedSections)
	      });
	    } catch (cause) {
	      const rollbackFailures = [];
	      for (const rollback of rollbacks)
	        try {
	          await rollback();
	        } catch (rollbackCause) {
	          rollbackFailures.push(rollbackCause);
	        }
	      throw rollbackFailures.length ? new Error("设置配置写入失败且回滚不完整", {
	        cause: new AggregateError([cause, ...rollbackFailures])
	      }) : cause;
	    }
	  }
	}
}, "de1850d0832f75d8e437786b0c0f3eea5e4989cf60012e3d610c43333e715ccb");

/* Source: lite/src/stream/reply-tree-viewport-layout.ts */
runtime.register("src/stream/reply-tree-viewport-layout.js", function(module, exports, require) {
	var reply_tree_viewport_layout_exports = {};
	__export(reply_tree_viewport_layout_exports, {
	  ReplyTreeViewportLayout: () => ReplyTreeViewportLayout
	});
	module.exports = __toCommonJS(reply_tree_viewport_layout_exports);
	function lowerBoundEndingAfter(count, endAt, offset) {
	  let low = 0, high = count;
	  for (; low < high; ) {
	    const middle = Math.floor((low + high) / 2);
	    endAt(middle) <= offset ? low = middle + 1 : high = middle;
	  }
	  return low;
	}
	function lowerBoundStartingAtOrAfter(count, startAt, offset) {
	  let low = 0, high = count;
	  for (; low < high; ) {
	    const middle = Math.floor((low + high) / 2);
	    startAt(middle) < offset ? low = middle + 1 : high = middle;
	  }
	  return low;
	}
	class ReplyTreeViewportLayout {
	  topology;
	  rootLayout;
	  #estimatedPostSize;
	  #measuredOwnSizes = /* @__PURE__ */ new Map();
	  #revision = "";
	  #branches = /* @__PURE__ */ new Map();
	  #visiblePostNumbers = Object.freeze([]);
	  constructor(topology, rootLayout, estimatedPostSize) {
	    if (!Number.isFinite(estimatedPostSize) || estimatedPostSize <= 0)
	      throw new RangeError("estimatedPostSize 必须是正有限数值");
	    this.topology = topology, this.rootLayout = rootLayout, this.#estimatedPostSize = estimatedPostSize;
	  }
	  /**
	   * 记录已完整投影节点自身的真实高度,不包含它的 replyList 子树。
	   *
	   * 节点退出正文窗口后,祖先壳和虚拟 spacer 必须继续占用相同高度;否则大正文会
	   * 在真实高度与固定估算之间反复切换,浏览器锚定补偿又会被误当成新的窗口输入。
	   */
	  measureOwnSize(postNumber, blockSize) {
	    this.#syncRevision();
	    const normalized = Math.round(Number(blockSize));
	    if (!Number.isFinite(normalized) || normalized <= 0 || (this.#measuredOwnSizes.get(postNumber) ?? this.#estimatedPostSize) === normalized) return !1;
	    this.#measuredOwnSizes.set(postNumber, normalized);
	    const rootPostNumber = this.topology.rootOf(postNumber), branch = rootPostNumber === void 0 ? void 0 : this.#branches.get(rootPostNumber), index = branch?.indexByPost.get(postNumber);
	    return branch && index !== void 0 && (branch.prefixDirtyFrom = Math.min(branch.prefixDirtyFrom, index)), !0;
	  }
	  /** 当前计划中真正与物理视口相交的 DFS 节点,供锚点读取缩小几何查询范围。 */
	  get visiblePostNumbers() {
	    return this.#visiblePostNumbers;
	  }
	  plan(rootWindow, input) {
	    this.#syncRevision();
	    const materializationStep = input.viewportSize * Math.max(0, input.materializationStepScreens ?? 0), materializationStart = materializationStep > 0 ? Math.floor(input.scrollOffset / materializationStep) * materializationStep : input.scrollOffset, overscanStart = Math.max(
	      0,
	      materializationStart - input.viewportSize * (input.overscanBeforeScreens ?? 1)
	    ), overscanEnd = materializationStart + materializationStep + input.viewportSize * (1 + (input.overscanAfterScreens ?? 1)), visibleStart = input.scrollOffset, visibleEnd = input.scrollOffset + input.viewportSize, candidates = [];
	    for (const rootPostNumber of rootWindow.postNumbers) {
	      const branch = this.#branch(rootPostNumber), branchPrefix = this.#branchPrefix(branch), rootStart = this.rootLayout.offsetOf(rootPostNumber), rootSize = this.rootLayout.blockSizeOf(rootPostNumber);
	      if (rootStart === void 0 || rootSize === void 0 || branch.entries.length === 0) continue;
	      const branchSize = branchPrefix.at(-1) ?? 0;
	      if (overscanEnd <= rootStart || overscanStart >= rootStart + rootSize) continue;
	      const localStart = Math.min(
	        Math.max(0, branchSize - 1),
	        Math.max(0, overscanStart - rootStart)
	      ), localEnd = Math.min(
	        branchSize,
	        Math.max(localStart + 1, overscanEnd - rootStart)
	      ), startIndex = lowerBoundEndingAfter(
	        branch.entries.length,
	        (index) => branchPrefix[index + 1] ?? 0,
	        localStart
	      ), endIndex = lowerBoundStartingAtOrAfter(
	        branch.entries.length,
	        (index) => branchPrefix[index] ?? 0,
	        localEnd
	      );
	      for (let index = startIndex; index < Math.max(startIndex + 1, endIndex) && index < branch.entries.length; index += 1)
	        candidates.push(Object.freeze({
	          branch,
	          entryIndex: index,
	          absoluteStart: rootStart + (branchPrefix[index] ?? 0),
	          absoluteEnd: rootStart + (branchPrefix[index + 1] ?? 0)
	        }));
	    }
	    this.#visiblePostNumbers = Object.freeze([
	      ...new Set(candidates.filter(
	        (candidate) => candidate.absoluteEnd > visibleStart && candidate.absoluteStart < visibleEnd
	      ).map(
	        (candidate) => candidate.branch.entries[candidate.entryIndex].postNumber
	      ))
	    ]);
	    const content = new Set(this.#budget(
	      candidates,
	      visibleStart,
	      visibleEnd,
	      input.maxMountedPostCount
	    )), preservePostNumber = input.preservePostNumber;
	    if (preservePostNumber !== void 0) {
	      const preserveRootPostNumber = this.topology.rootOf(preservePostNumber);
	      preserveRootPostNumber !== void 0 && rootWindow.postNumbers.includes(preserveRootPostNumber) && content.add(preservePostNumber);
	    }
	    const mounted = new Set(content);
	    for (const postNumber of content) {
	      let parentPostNumber = this.topology.parentOf(postNumber);
	      for (; parentPostNumber != null && !mounted.has(parentPostNumber); )
	        mounted.add(parentPostNumber), parentPostNumber = this.topology.parentOf(parentPostNumber);
	    }
	    const shells = new Set(
	      [...mounted].filter((postNumber) => !content.has(postNumber))
	    ), ownSizes = /* @__PURE__ */ new Map(), mountedChildrenByParent = /* @__PURE__ */ new Map();
	    for (const postNumber of mounted) {
	      const parentPostNumber = this.topology.parentOf(postNumber);
	      if (parentPostNumber == null || !mounted.has(parentPostNumber)) continue;
	      const children = mountedChildrenByParent.get(parentPostNumber) ?? [];
	      children.push(postNumber), mountedChildrenByParent.set(parentPostNumber, children);
	    }
	    const childLayouts = /* @__PURE__ */ new Map();
	    for (const postNumber of mounted) {
	      const rootPostNumber = this.topology.rootOf(postNumber);
	      if (rootPostNumber === void 0) continue;
	      const branch = this.#branch(rootPostNumber), branchPrefix = this.#branchPrefix(branch), index = branch.indexByPost.get(postNumber), rootSize = this.rootLayout.blockSizeOf(rootPostNumber);
	      if (index === void 0 || rootSize === void 0) continue;
	      ownSizes.set(postNumber, this.#ownSize(postNumber));
	      const parentEntry = branch.entries[index], mountedChildren = mountedChildrenByParent.get(postNumber) ?? [];
	      mountedChildren.sort(
	        (left, right) => (branch.indexByPost.get(left) ?? 0) - (branch.indexByPost.get(right) ?? 0)
	      );
	      const beforeSizes = [];
	      let cursor = index + 1;
	      for (const childPostNumber of mountedChildren) {
	        const childIndex = branch.indexByPost.get(childPostNumber);
	        childIndex !== void 0 && (beforeSizes.push(Math.max(
	          0,
	          (branchPrefix[childIndex] ?? 0) - (branchPrefix[cursor] ?? 0)
	        )), cursor = branch.entries[childIndex].subtreeEndIndex);
	      }
	      childLayouts.set(postNumber, Object.freeze({
	        postNumbers: Object.freeze([...mountedChildren]),
	        beforeSizes: Object.freeze(beforeSizes),
	        afterSize: Math.max(
	          0,
	          (branchPrefix[parentEntry.subtreeEndIndex] ?? 0) - (branchPrefix[cursor] ?? 0)
	        )
	      }));
	    }
	    const rootVirtualInsets = this.#externalizeWindowEdgeSpacers(
	      rootWindow,
	      content,
	      shells,
	      ownSizes,
	      childLayouts
	    );
	    return Object.freeze({
	      mountedPostNumbers: mounted,
	      contentPostNumbers: content,
	      shellPostNumbers: shells,
	      ownSizes,
	      rootVirtualInsets,
	      childLayouts
	    });
	  }
	  /**
	   * 窗口首尾之外的 DFS 高度属于流级占位,不应留在递归 replyList 内穿过视口。
	   * 这里只改占位的归属;正文集合、祖先闭包和半屏物化分段均保持不变。
	   */
	  #externalizeWindowEdgeSpacers(rootWindow, content, shells, ownSizes, childLayouts) {
	    const firstRoot = rootWindow.postNumbers[0], lastRoot = rootWindow.postNumbers.at(-1), edgeRoots = new Set(
	      [firstRoot, lastRoot].filter(
	        (postNumber) => postNumber !== void 0
	      )
	    ), insets = /* @__PURE__ */ new Map();
	    for (const rootPostNumber of edgeRoots) {
	      const branch = this.#branch(rootPostNumber);
	      let firstIndex = Number.POSITIVE_INFINITY, lastIndex = -1;
	      for (const postNumber of content) {
	        const index = branch.indexByPost.get(postNumber);
	        index !== void 0 && (firstIndex = Math.min(firstIndex, index), lastIndex = Math.max(lastIndex, index));
	      }
	      if (lastIndex < 0) continue;
	      const prefix = this.#branchPrefix(branch), beforeSize = rootPostNumber === firstRoot ? prefix[firstIndex] ?? 0 : 0, afterSize = rootPostNumber === lastRoot ? Math.max(
	        0,
	        (prefix.at(-1) ?? 0) - (prefix[lastIndex + 1] ?? 0)
	      ) : 0;
	      beforeSize > 0 && this.#trimLeadingPath(
	        branch,
	        firstIndex,
	        shells,
	        ownSizes,
	        childLayouts
	      ), afterSize > 0 && this.#trimTrailingPath(branch, lastIndex, childLayouts), (beforeSize > 0 || afterSize > 0) && insets.set(rootPostNumber, Object.freeze({ beforeSize, afterSize }));
	    }
	    return insets;
	  }
	  #trimLeadingPath(branch, entryIndex, shells, ownSizes, childLayouts) {
	    const path = this.#entryPath(branch, entryIndex);
	    for (let index = 0; index + 1 < path.length; index += 1) {
	      const parentPostNumber = path[index], childPostNumber = path[index + 1];
	      shells.has(parentPostNumber) && ownSizes.set(parentPostNumber, 0);
	      const layout = childLayouts.get(parentPostNumber), childIndex = layout?.postNumbers.indexOf(childPostNumber) ?? -1;
	      if (!layout || childIndex < 0 || layout.beforeSizes[childIndex] === 0)
	        continue;
	      const beforeSizes = [...layout.beforeSizes];
	      beforeSizes[childIndex] = 0, childLayouts.set(parentPostNumber, Object.freeze({
	        ...layout,
	        beforeSizes: Object.freeze(beforeSizes)
	      }));
	    }
	  }
	  #trimTrailingPath(branch, entryIndex, childLayouts) {
	    for (const postNumber of this.#entryPath(branch, entryIndex)) {
	      const layout = childLayouts.get(postNumber);
	      !layout || layout.afterSize === 0 || childLayouts.set(postNumber, Object.freeze({
	        ...layout,
	        afterSize: 0
	      }));
	    }
	  }
	  #entryPath(branch, entryIndex) {
	    const path = [];
	    let entry = branch.entries[entryIndex];
	    for (; entry && (path.push(entry.postNumber), entry.parentPostNumber !== null); ) {
	      const parentIndex = branch.indexByPost.get(entry.parentPostNumber);
	      entry = parentIndex === void 0 ? void 0 : branch.entries[parentIndex];
	    }
	    return Object.freeze(path.reverse());
	  }
	  offsetOf(postNumber) {
	    this.#syncRevision();
	    const rootPostNumber = this.topology.rootOf(postNumber);
	    if (rootPostNumber === void 0) return;
	    const rootOffset = this.rootLayout.offsetOf(rootPostNumber), rootSize = this.rootLayout.blockSizeOf(rootPostNumber);
	    if (rootOffset === void 0 || rootSize === void 0) return;
	    const branch = this.#branch(rootPostNumber), branchPrefix = this.#branchPrefix(branch), index = branch.indexByPost.get(postNumber);
	    if (!(index === void 0 || !branch.entries.length))
	      return rootOffset + Math.min(
	        branchPrefix[index] ?? 0,
	        Math.max(0, rootSize - 1)
	      );
	  }
	  #budget(candidates, visibleStart, visibleEnd, rawBudget) {
	    if (!candidates.length) return /* @__PURE__ */ new Set();
	    const visibleIndexes = [];
	    for (let index = 0; index < candidates.length; index += 1) {
	      const candidate = candidates[index];
	      candidate.absoluteEnd > visibleStart && candidate.absoluteStart < visibleEnd && visibleIndexes.push(index);
	    }
	    const firstVisible = visibleIndexes[0] ?? 0, lastVisible = visibleIndexes.at(-1) ?? firstVisible, budget = rawBudget === void 0 ? candidates.length : Math.max(rawBudget, lastVisible - firstVisible + 1);
	    let start = firstVisible, end = lastVisible + 1;
	    for (; end - start < budget && (start > 0 || end < candidates.length); ) {
	      const beforeDistance = start > 0 ? Math.max(0, visibleStart - candidates[start - 1].absoluteEnd) : Number.POSITIVE_INFINITY, afterDistance = end < candidates.length ? Math.max(0, candidates[end].absoluteStart - visibleEnd) : Number.POSITIVE_INFINITY;
	      if (beforeDistance <= afterDistance && start > 0) start -= 1;
	      else if (end < candidates.length) end += 1;
	      else break;
	    }
	    return new Set(
	      candidates.slice(start, end).map(
	        (candidate) => candidate.branch.entries[candidate.entryIndex].postNumber
	      )
	    );
	  }
	  #syncRevision() {
	    this.#revision !== this.topology.revision && (this.#revision = this.topology.revision, this.#branches.clear(), this.#visiblePostNumbers = Object.freeze([]));
	  }
	  #branch(rootPostNumber) {
	    const cached = this.#branches.get(rootPostNumber);
	    if (cached) return cached;
	    const entries = [], indexByPost = /* @__PURE__ */ new Map(), stack = [{
	      postNumber: rootPostNumber,
	      parentPostNumber: null,
	      depth: 0,
	      closing: !1
	    }];
	    for (; stack.length; ) {
	      const current = stack.pop();
	      if (current.closing) {
	        const index2 = indexByPost.get(current.postNumber);
	        index2 !== void 0 && (entries[index2].subtreeEndIndex = entries.length);
	        continue;
	      }
	      const index = entries.length;
	      indexByPost.set(current.postNumber, index), entries.push({
	        postNumber: current.postNumber,
	        parentPostNumber: current.parentPostNumber,
	        depth: current.depth,
	        subtreeEndIndex: index + 1
	      }), stack.push({ ...current, closing: !0 });
	      const children = this.topology.childrenOf(current.postNumber);
	      for (let childIndex = children.length - 1; childIndex >= 0; childIndex -= 1)
	        stack.push({
	          postNumber: children[childIndex],
	          parentPostNumber: current.postNumber,
	          depth: current.depth + 1,
	          closing: !1
	        });
	    }
	    const projection = {
	      entries: Object.freeze(entries),
	      indexByPost,
	      prefix: new Array(entries.length + 1).fill(0),
	      prefixDirtyFrom: 0
	    };
	    return this.#branches.set(rootPostNumber, projection), projection;
	  }
	  #ownSize(postNumber) {
	    return this.#measuredOwnSizes.get(postNumber) ?? this.#estimatedPostSize;
	  }
	  #branchPrefix(branch) {
	    for (let index = branch.prefixDirtyFrom; index < branch.entries.length; index += 1)
	      branch.prefix[index + 1] = (branch.prefix[index] ?? 0) + this.#ownSize(branch.entries[index].postNumber);
	    return branch.prefixDirtyFrom = branch.entries.length, branch.prefix;
	  }
	}
}, "a5c984546f0ef4ebd63fd285cb274678fb101407c6aa63f87f9104deff2e0efc");

/* Source: lite/src/stream/reply-tree-virtual-layout-controller.ts */
runtime.register("src/stream/reply-tree-virtual-layout-controller.js", function(module, exports, require) {
	var reply_tree_virtual_layout_controller_exports = {};
	__export(reply_tree_virtual_layout_controller_exports, {
	  ReplyTreeVirtualLayoutController: () => ReplyTreeVirtualLayoutController
	});
	module.exports = __toCommonJS(reply_tree_virtual_layout_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReplyTreeVirtualLayoutController {
	  repository;
	  topology;
	  layout;
	  scope;
	  #rootBranches = null;
	  constructor(repository, layout, parentScope, topology = repository.topology) {
	    this.repository = repository, this.topology = topology, this.layout = layout, this.scope = import_lifecycle.LifecycleScope.ownedBy(parentScope), this.syncRoots(), repository.changes.subscribe(() => this.syncRoots(), this.scope);
	  }
	  syncRoots() {
	    const rootBranches = this.topology.rootBranches();
	    rootBranches !== this.#rootBranches && (this.#rootBranches = rootBranches, this.layout.setRoots(rootBranches));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
}, "02fe389ab682c83811aa4b0ed77520be73670dd237cf833edf11c9c2f25ac4e7");

/* Source: lite/src/stream/virtual-root-layout.ts */
runtime.register("src/stream/virtual-root-layout.js", function(module, exports, require) {
	var virtual_root_layout_exports = {};
	__export(virtual_root_layout_exports, {
	  VirtualRootLayout: () => VirtualRootLayout
	});
	module.exports = __toCommonJS(virtual_root_layout_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function finiteNonNegative(value, name) {
	  if (!Number.isFinite(value) || value < 0)
	    throw new RangeError(`${name} 必须是非负有限数值`);
	  return value;
	}
	function positiveSize(value, name) {
	  if (!Number.isFinite(value) || value <= 0)
	    throw new RangeError(`${name} 必须是正有限数值`);
	  return value;
	}
	function assertPostNumber(value) {
	  try {
	    (0, import_identifiers.discoursePostNumber)(value);
	  } catch {
	    throw new RangeError("根楼层号必须是正安全整数");
	  }
	}
	function positiveSafeInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value <= 0)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function nonNegativeSafeInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 0)
	    throw new RangeError(`${name} 必须是非负安全整数`);
	  return value;
	}
	class VirtualRootLayout {
	  #estimatedSize;
	  #estimateSubtreeSize;
	  #measuredSizes = /* @__PURE__ */ new Map();
	  #postNumbers = [];
	  #subtreePostCounts = [];
	  #unloadedPostCountsBefore = [];
	  #segmentStartByIndex = [];
	  #segmentEndByIndex = [];
	  #indexByPost = /* @__PURE__ */ new Map();
	  #prefix = [0];
	  #subtreeCountPrefix = [0];
	  #dirtyFrom = 0;
	  constructor(estimatedSize, estimateSubtreeSize = !1) {
	    this.#estimatedSize = positiveSize(estimatedSize, "estimatedSize"), this.#estimateSubtreeSize = estimateSubtreeSize;
	  }
	  setRoots(roots) {
	    const previousSubtreePostCountByPost = new Map(
	      this.#postNumbers.map((postNumber, index) => [
	        postNumber,
	        this.#subtreePostCounts[index] ?? 1
	      ])
	    ), unique = /* @__PURE__ */ new Set(), subtreePostCountByPost = /* @__PURE__ */ new Map(), unloadedPostCountBeforeByPost = /* @__PURE__ */ new Map();
	    for (const root of roots) {
	      const postNumber = typeof root == "number" ? root : root.postNumber;
	      if (assertPostNumber(postNumber), unique.has(postNumber)) throw new Error(`根楼层 #${postNumber} 重复`);
	      unique.add(postNumber), subtreePostCountByPost.set(
	        postNumber,
	        typeof root == "number" ? 1 : positiveSafeInteger(root.subtreePostCount, "subtreePostCount")
	      );
	      const unloadedPostCountBefore = typeof root == "number" ? 0 : nonNegativeSafeInteger(
	        root.unloadedPostCountBefore ?? 0,
	        "unloadedPostCountBefore"
	      );
	      if (unloadedPostCountBefore >= postNumber)
	        throw new RangeError(
	          "unloadedPostCountBefore 必须小于当前根楼层号"
	        );
	      unloadedPostCountBeforeByPost.set(
	        postNumber,
	        unloadedPostCountBefore
	      );
	    }
	    this.#postNumbers = [...unique].sort((left, right) => left - right), this.#subtreePostCounts = this.#postNumbers.map(
	      (postNumber) => subtreePostCountByPost.get(postNumber) ?? 1
	    ), this.#unloadedPostCountsBefore = this.#postNumbers.map(
	      (postNumber) => unloadedPostCountBeforeByPost.get(postNumber) ?? 0
	    ), this.#segmentStartByIndex = new Array(this.#postNumbers.length);
	    let segmentStart = 0;
	    for (let index = 0; index < this.#postNumbers.length; index += 1)
	      (this.#unloadedPostCountsBefore[index] ?? 0) > 0 && (segmentStart = index), this.#segmentStartByIndex[index] = segmentStart;
	    this.#segmentEndByIndex = new Array(this.#postNumbers.length);
	    let segmentEnd = this.#postNumbers.length;
	    for (let index = this.#postNumbers.length - 1; index >= 0; index -= 1)
	      index + 1 < this.#postNumbers.length && (this.#unloadedPostCountsBefore[index + 1] ?? 0) > 0 && (segmentEnd = index + 1), this.#segmentEndByIndex[index] = segmentEnd;
	    for (const postNumber of this.#measuredSizes.keys()) {
	      const subtreePostCountChanged = this.#estimateSubtreeSize && previousSubtreePostCountByPost.get(postNumber) !== subtreePostCountByPost.get(postNumber);
	      (!unique.has(postNumber) || subtreePostCountChanged) && this.#measuredSizes.delete(postNumber);
	    }
	    this.#indexByPost = new Map(
	      this.#postNumbers.map((postNumber, index) => [postNumber, index])
	    ), this.#prefix = new Array(this.#postNumbers.length + 1).fill(0), this.#subtreeCountPrefix = new Array(this.#postNumbers.length + 1).fill(0);
	    for (let index = 0; index < this.#postNumbers.length; index += 1)
	      this.#subtreeCountPrefix[index + 1] = (this.#subtreeCountPrefix[index] ?? 0) + (this.#subtreePostCounts[index] ?? 1);
	    this.#dirtyFrom = 0;
	  }
	  roots() {
	    return Object.freeze([...this.#postNumbers]);
	  }
	  offsetOf(postNumber) {
	    assertPostNumber(postNumber);
	    const index = this.#indexByPost.get(postNumber);
	    if (index !== void 0)
	      return this.#ensurePrefix(), this.#prefix[index] ?? 0;
	  }
	  blockSizeOf(postNumber) {
	    assertPostNumber(postNumber);
	    const index = this.#indexByPost.get(postNumber);
	    return index === void 0 ? void 0 : this.#sizeAt(index);
	  }
	  measure(postNumber, blockSize, anchorPostNumber) {
	    assertPostNumber(postNumber);
	    const normalizedSize = positiveSize(blockSize, "blockSize"), index = this.#indexByPost.get(postNumber);
	    if (index === void 0)
	      return Object.freeze({ changed: !1, sizeDelta: 0, scrollCompensation: 0 });
	    const previousSize = this.#sizeAt(index);
	    if (previousSize === normalizedSize)
	      return Object.freeze({ changed: !1, sizeDelta: 0, scrollCompensation: 0 });
	    this.#measuredSizes.set(postNumber, normalizedSize), this.#dirtyFrom = Math.min(this.#dirtyFrom, index);
	    const sizeDelta = normalizedSize - previousSize, anchorIndex = anchorPostNumber === void 0 ? void 0 : this.#indexByPost.get(anchorPostNumber);
	    return Object.freeze({
	      changed: !0,
	      sizeDelta,
	      scrollCompensation: anchorIndex !== void 0 && index < anchorIndex ? sizeDelta : 0
	    });
	  }
	  window(input) {
	    const scrollOffset = finiteNonNegative(input.scrollOffset, "scrollOffset"), viewportSize = positiveSize(input.viewportSize, "viewportSize"), beforeScreens = finiteNonNegative(
	      input.overscanBeforeScreens ?? 1,
	      "overscanBeforeScreens"
	    ), afterScreens = finiteNonNegative(
	      input.overscanAfterScreens ?? 1,
	      "overscanAfterScreens"
	    ), materializationStepScreens = finiteNonNegative(
	      input.materializationStepScreens ?? 0,
	      "materializationStepScreens"
	    ), maxMountedPostCount = input.maxMountedPostCount === void 0 ? void 0 : positiveSafeInteger(
	      input.maxMountedPostCount,
	      "maxMountedPostCount"
	    );
	    this.#ensurePrefix();
	    const totalSize = this.#prefix.at(-1) ?? 0;
	    if (!this.#postNumbers.length)
	      return Object.freeze({
	        startIndex: 0,
	        endIndex: 0,
	        postNumbers: Object.freeze([]),
	        visiblePostNumbers: Object.freeze([]),
	        atStart: !0,
	        atEnd: !0,
	        beforeSpacer: 0,
	        afterSpacer: 0,
	        hasUnloadedGapBefore: !1,
	        hasUnloadedGapAfter: !1,
	        distanceToSegmentStart: 0,
	        distanceToSegmentEnd: 0,
	        afterSegmentSpacer: 0,
	        totalSize: 0
	      });
	    const materializationStep = viewportSize * materializationStepScreens, materializationStart = materializationStep > 0 ? Math.floor(scrollOffset / materializationStep) * materializationStep : scrollOffset, rangeStart = Math.max(
	      0,
	      materializationStart - viewportSize * beforeScreens
	    ), rangeEnd = Math.min(
	      totalSize,
	      materializationStart + materializationStep + viewportSize * (1 + afterScreens)
	    );
	    let visibleStartIndex = this.#firstBlockEndingAfter(
	      Math.min(scrollOffset, Math.max(0, totalSize - 1))
	    );
	    const visibleRootEnd = (this.#prefix[visibleStartIndex] ?? 0) + this.#sizeAt(visibleStartIndex), nextVisibleIndex = visibleStartIndex + 1;
	    visibleRootEnd <= scrollOffset && nextVisibleIndex < this.#postNumbers.length && (this.#prefix[nextVisibleIndex] ?? 0) < Math.min(totalSize, scrollOffset + viewportSize) && (visibleStartIndex = nextVisibleIndex);
	    const segmentStartIndex = this.#segmentStartByIndex[visibleStartIndex] ?? 0, segmentEndIndex = this.#segmentEndByIndex[visibleStartIndex] ?? this.#postNumbers.length, unloadedGap = this.#unloadedGapAt(
	      scrollOffset + viewportSize / 2
	    ), overscanStartIndex = Math.max(
	      segmentStartIndex,
	      this.#firstBlockEndingAfter(rangeStart)
	    ), overscanEndIndex = Math.min(
	      segmentEndIndex,
	      this.#postNumbers.length,
	      Math.max(
	        overscanStartIndex + 1,
	        this.#firstBlockStartingAtOrAfter(rangeEnd)
	      )
	    ), visibleEndIndex = Math.min(
	      segmentEndIndex,
	      Math.max(
	        visibleStartIndex + 1,
	        this.#firstBlockStartingAtOrAfter(
	          Math.min(totalSize, scrollOffset + viewportSize)
	        )
	      )
	    );
	    let { startIndex, endIndex } = maxMountedPostCount === void 0 ? {
	      startIndex: overscanStartIndex,
	      endIndex: overscanEndIndex
	    } : this.#budgetedRange({
	      overscanStartIndex,
	      overscanEndIndex,
	      visibleStartIndex,
	      visibleEndIndex,
	      maxMountedPostCount,
	      scrollOffset,
	      viewportSize
	    });
	    const preserveRootIndex = input.preserveRootPostNumber === void 0 ? void 0 : this.#indexByPost.get(input.preserveRootPostNumber);
	    preserveRootIndex !== void 0 && preserveRootIndex >= segmentStartIndex && preserveRootIndex < segmentEndIndex && (startIndex = Math.min(startIndex, preserveRootIndex), endIndex = Math.max(endIndex, preserveRootIndex + 1));
	    const boundedEnd = Math.min(this.#postNumbers.length, endIndex), visiblePostNumbers = [];
	    for (let index = visibleStartIndex; index < visibleEndIndex; index += 1) {
	      const rootStart = this.#prefix[index] ?? 0;
	      rootStart + this.#sizeAt(index) > scrollOffset && rootStart < scrollOffset + viewportSize && visiblePostNumbers.push(this.#postNumbers[index]);
	    }
	    const mountedEnd = boundedEnd > startIndex ? (this.#prefix[boundedEnd - 1] ?? 0) + this.#sizeAt(boundedEnd - 1) : this.#prefix[startIndex] ?? 0, segmentContentEnd = segmentEndIndex > segmentStartIndex ? (this.#prefix[segmentEndIndex - 1] ?? 0) + this.#sizeAt(segmentEndIndex - 1) : mountedEnd, segmentContentStart = this.#prefix[segmentStartIndex] ?? 0, hasUnloadedGapBefore = (this.#unloadedPostCountsBefore[segmentStartIndex] ?? 0) > 0, hasUnloadedGapAfter = segmentEndIndex < this.#postNumbers.length && (this.#unloadedPostCountsBefore[segmentEndIndex] ?? 0) > 0, segmentStartPostNumber = this.#postNumbers[segmentStartIndex], segmentEndPostNumber = this.#postNumbers[segmentEndIndex - 1], unloadedGapAfterAnchorPostNumber = hasUnloadedGapAfter ? (0, import_identifiers.discoursePostNumber)(
	      this.#postNumbers[segmentEndIndex] - (this.#unloadedPostCountsBefore[segmentEndIndex] ?? 0) - 1
	    ) : void 0;
	    return Object.freeze({
	      startIndex,
	      endIndex: boundedEnd,
	      postNumbers: Object.freeze(this.#postNumbers.slice(startIndex, boundedEnd)),
	      visiblePostNumbers: Object.freeze(visiblePostNumbers),
	      atStart: scrollOffset <= 10,
	      atEnd: scrollOffset + viewportSize >= Math.max(0, totalSize - 16),
	      beforeSpacer: this.#prefix[startIndex] ?? 0,
	      afterSpacer: Math.max(0, totalSize - mountedEnd),
	      hasUnloadedGapBefore,
	      hasUnloadedGapAfter,
	      segmentStartPostNumber,
	      segmentEndPostNumber,
	      ...hasUnloadedGapBefore ? { unloadedGapBeforeAnchorPostNumber: segmentStartPostNumber } : {},
	      ...unloadedGapAfterAnchorPostNumber === void 0 ? {} : { unloadedGapAfterAnchorPostNumber },
	      distanceToSegmentStart: Math.max(
	        0,
	        scrollOffset - segmentContentStart
	      ),
	      distanceToSegmentEnd: Math.max(
	        0,
	        segmentContentEnd - (scrollOffset + viewportSize)
	      ),
	      afterSegmentSpacer: Math.max(0, segmentContentEnd - mountedEnd),
	      ...unloadedGap === void 0 || visiblePostNumbers.length > 0 && unloadedGap.nextIndex <= visibleStartIndex ? {} : {
	        unloadedGapTargetPostNumber: unloadedGap.targetPostNumber,
	        unloadedGapSide: unloadedGap.nextIndex <= visibleStartIndex ? "before" : "after"
	      },
	      totalSize
	    });
	  }
	  #budgetedRange(input) {
	    let startIndex = input.visibleStartIndex, endIndex = input.visibleEndIndex, mountedPostCount = this.#subtreePostCountBetween(startIndex, endIndex), beforeBlocked = !1, afterBlocked = !1;
	    for (; !beforeBlocked && startIndex > input.overscanStartIndex || !afterBlocked && endIndex < input.overscanEndIndex; ) {
	      const beforeIndex = startIndex - 1, afterIndex = endIndex, beforeDistance = !beforeBlocked && beforeIndex >= input.overscanStartIndex ? Math.max(
	        0,
	        input.scrollOffset - (this.#prefix[beforeIndex + 1] ?? 0)
	      ) : Number.POSITIVE_INFINITY, afterDistance = !afterBlocked && afterIndex < input.overscanEndIndex ? Math.max(
	        0,
	        (this.#prefix[afterIndex] ?? 0) - (input.scrollOffset + input.viewportSize)
	      ) : Number.POSITIVE_INFINITY;
	      if (beforeDistance === Number.POSITIVE_INFINITY && afterDistance === Number.POSITIVE_INFINITY)
	        break;
	      const addBefore = beforeDistance <= afterDistance, candidateIndex = addBefore ? beforeIndex : afterIndex, candidateWeight = this.#subtreePostCounts[candidateIndex] ?? 1;
	      if (mountedPostCount + candidateWeight > input.maxMountedPostCount) {
	        addBefore ? beforeBlocked = !0 : afterBlocked = !0;
	        continue;
	      }
	      mountedPostCount += candidateWeight, addBefore ? startIndex = candidateIndex : endIndex = candidateIndex + 1;
	    }
	    return Object.freeze({ startIndex, endIndex });
	  }
	  #subtreePostCountBetween(startIndex, endIndex) {
	    return (this.#subtreeCountPrefix[endIndex] ?? 0) - (this.#subtreeCountPrefix[startIndex] ?? 0);
	  }
	  #sizeAt(index) {
	    const postNumber = this.#postNumbers[index];
	    return this.#measuredSizes.get(postNumber) ?? this.#estimatedSize * (this.#estimateSubtreeSize ? this.#subtreePostCounts[index] ?? 1 : 1);
	  }
	  #ensurePrefix() {
	    const start = Math.min(this.#dirtyFrom, this.#postNumbers.length);
	    start === 0 && (this.#prefix[0] = this.#estimatedSize * (this.#unloadedPostCountsBefore[0] ?? 0));
	    for (let index = start; index < this.#postNumbers.length; index += 1)
	      this.#prefix[index + 1] = (this.#prefix[index] ?? 0) + this.#sizeAt(index) + this.#estimatedSize * (this.#unloadedPostCountsBefore[index + 1] ?? 0);
	    this.#prefix.length = this.#postNumbers.length + 1, this.#dirtyFrom = this.#postNumbers.length;
	  }
	  #firstBlockEndingAfter(offset) {
	    let low = 0, high = this.#postNumbers.length;
	    for (; low < high; ) {
	      const middle = Math.floor((low + high) / 2);
	      (this.#prefix[middle + 1] ?? 0) <= offset ? low = middle + 1 : high = middle;
	    }
	    return Math.min(low, this.#postNumbers.length - 1);
	  }
	  #firstBlockStartingAtOrAfter(offset) {
	    let low = 0, high = this.#postNumbers.length;
	    for (; low < high; ) {
	      const middle = Math.floor((low + high) / 2);
	      (this.#prefix[middle] ?? 0) < offset ? low = middle + 1 : high = middle;
	    }
	    return low;
	  }
	  #unloadedGapAt(offset) {
	    if (!Number.isFinite(offset) || offset < 0 || !this.#postNumbers.length)
	      return;
	    const nextIndex = this.#firstBlockStartingAtOrAfter(offset);
	    if (nextIndex >= this.#postNumbers.length) return;
	    const unloadedPostCount = this.#unloadedPostCountsBefore[nextIndex] ?? 0;
	    if (unloadedPostCount <= 0) return;
	    const gapEnd = this.#prefix[nextIndex] ?? 0, gapStart = gapEnd - this.#estimatedSize * unloadedPostCount;
	    if (offset < gapStart || offset >= gapEnd) return;
	    const nextPostNumber = this.#postNumbers[nextIndex], missingOffset = Math.min(
	      unloadedPostCount - 1,
	      Math.max(0, Math.floor((offset - gapStart) / this.#estimatedSize))
	    );
	    return Object.freeze({
	      nextIndex,
	      targetPostNumber: (0, import_identifiers.discoursePostNumber)(
	        nextPostNumber - unloadedPostCount + missingOffset
	      )
	    });
	  }
	}
}, "06ff523dcdcc3f801208c43af97b4dc459d31a1b529dd532e10b1743058901e9");

/* Source: lite/src/stream/virtual-stream-dom-controller.ts */
runtime.register("src/stream/virtual-stream-dom-controller.js", function(module, exports, require) {
	var virtual_stream_dom_controller_exports = {};
	__export(virtual_stream_dom_controller_exports, {
	  VirtualStreamDomController: () => VirtualStreamDomController
	});
	module.exports = __toCommonJS(virtual_stream_dom_controller_exports);
	class VirtualStreamDomController {
	  repository;
	  layout;
	  streamView;
	  domOwner;
	  #prepareRoots;
	  #roots;
	  #resolveGapPlaceholder;
	  #connectedRoots = /* @__PURE__ */ new Set();
	  #committed = !1;
	  constructor(repository, layout, streamView, domOwner, options = {}) {
	    if (streamView.slots.rootList !== domOwner.rootList)
	      throw new Error("ReplyTreeDomOwner 必须挂载到 VirtualStreamView.rootList");
	    this.repository = repository, this.layout = layout, this.streamView = streamView, this.domOwner = domOwner, this.#prepareRoots = options.prepareRoots ?? (() => {
	    }), this.#roots = options.roots ?? (() => repository.topology.roots()), this.#resolveGapPlaceholder = options.resolveGapPlaceholder ?? ((window) => window.unloadedGapTargetPostNumber === void 0 || window.unloadedGapSide === void 0 ? null : Object.freeze({
	      side: window.unloadedGapSide,
	      targetPostNumber: window.unloadedGapTargetPostNumber
	    }));
	  }
	  commit(input) {
	    const window = this.layout.window(input), mountPlan = this.#prepareRoots(window.postNumbers, input, window), desired = new Set(window.postNumbers), connectedBefore = this.#committed ? this.#connectedRoots : new Set(this.#roots().filter((postNumber) => {
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      return !!root && this.streamView.slots.rootList.contains(root);
	    })), tree = this.domOwner.sync(desired, mountPlan);
	    for (let index = 0; index < window.postNumbers.length; index += 1) {
	      const postNumber = window.postNumbers[index], root = this.domOwner.view(postNumber)?.slots.root, zebra = (window.startIndex + index) % 2 === 1;
	      root?.classList.contains("ldp-zebra-alt") !== zebra && root?.classList.toggle("ldp-zebra-alt", zebra);
	      const beforeZebra = index + 1 < window.postNumbers.length && (window.startIndex + index + 1) % 2 === 1;
	      root?.classList.contains("ldp-before-zebra") !== beforeZebra && root?.classList.toggle("ldp-before-zebra", beforeZebra);
	    }
	    const connectedAfter = new Set(
	      [...desired].filter(
	        (postNumber) => {
	          const root = this.domOwner.view(postNumber)?.slots.root;
	          return !!root && this.streamView.slots.rootList.contains(root);
	        }
	      )
	    ), attachedRoots = [...connectedAfter].filter((postNumber) => !connectedBefore.has(postNumber)), detachedRoots = [...connectedBefore].filter((postNumber) => !connectedAfter.has(postNumber));
	    this.#connectedRoots = connectedAfter, this.#committed = !0;
	    const firstRootInset = window.postNumbers[0] === void 0 ? void 0 : mountPlan?.rootVirtualInsets?.get(window.postNumbers[0]), lastRootPostNumber = window.postNumbers.at(-1), lastRootInset = lastRootPostNumber === void 0 ? void 0 : mountPlan?.rootVirtualInsets?.get(lastRootPostNumber);
	    return this.streamView.setSpacerSizes(
	      window.beforeSpacer + (firstRootInset?.beforeSize ?? 0),
	      window.afterSpacer + (lastRootInset?.afterSize ?? 0)
	    ), this.streamView.setGapPlaceholder(
	      this.#resolveGapPlaceholder(window, input)
	    ), Object.freeze({
	      window,
	      tree,
	      attachedRoots: Object.freeze(
	        attachedRoots.sort((left, right) => left - right)
	      ),
	      detachedRoots: Object.freeze(
	        detachedRoots.sort((left, right) => left - right)
	      )
	    });
	  }
	}
}, "36cc128afd46bfe67839055dbae9fc260fa9519e584dd6eec0ea10ee2dcaa4ce");

/* Source: lite/src/stream/virtual-stream-frame-controller.ts */
runtime.register("src/stream/virtual-stream-frame-controller.js", function(module, exports, require) {
	var virtual_stream_frame_controller_exports = {};
	__export(virtual_stream_frame_controller_exports, {
	  VirtualStreamFrameController: () => VirtualStreamFrameController
	});
	module.exports = __toCommonJS(virtual_stream_frame_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_identifiers = require("../discourse/identifiers.js");
	function browserObserverFactory(callback) {
	  return new ResizeObserver((entries) => {
	    callback(
	      entries.map((entry) => {
	        const borderBox = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize;
	        return Object.freeze({
	          target: entry.target,
	          blockSize: borderBox?.blockSize ?? entry.contentRect.height
	        });
	      })
	    );
	  });
	}
	const browserFrameScheduler = Object.freeze({
	  request: (callback) => requestAnimationFrame(callback),
	  cancel: (handle) => cancelAnimationFrame(handle)
	}), SCROLL_OFFSET_EPSILON = 0.5;
	function postNumberFromElement(target) {
	  return (0, import_identifiers.tryDiscoursePostNumber)(target.getAttribute("data-post-number"));
	}
	class VirtualStreamFrameController {
	  domController;
	  scope;
	  #readWindowInput;
	  #applyScrollCompensation;
	  #shouldApplyScrollCompensation;
	  #shouldDeferMeasurements;
	  #onMeasurementsDeferred;
	  #resolveRootBlockSize;
	  #onCommit;
	  #observer;
	  #frames;
	  #observedRoots = /* @__PURE__ */ new Map();
	  #deferredMeasurements = /* @__PURE__ */ new Map();
	  #frameHandle = null;
	  #pendingCompensation = 0;
	  #measurementScrollOffset = null;
	  #lastCommit = null;
	  constructor(domController, options) {
	    this.domController = domController, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.scope), this.#readWindowInput = options.readWindowInput, this.#applyScrollCompensation = options.applyScrollCompensation, this.#shouldApplyScrollCompensation = options.shouldApplyScrollCompensation ?? (() => !0), this.#shouldDeferMeasurements = options.shouldDeferMeasurements ?? (() => !1), this.#onMeasurementsDeferred = options.onMeasurementsDeferred ?? (() => {
	    }), this.#resolveRootBlockSize = options.resolveRootBlockSize ?? ((_target, blockSize) => blockSize), this.#onCommit = options.onCommit, this.#frames = options.frameScheduler ?? browserFrameScheduler, this.#observer = (options.observerFactory ?? browserObserverFactory)((entries) => {
	      this.#recordMeasurements(entries);
	    }), this.scope.add(() => {
	      this.#frameHandle !== null && this.#frames.cancel(this.#frameHandle), this.#frameHandle = null, this.#clearMeasurementTransaction(), this.#observedRoots.clear(), this.#deferredMeasurements.clear(), this.#observer.disconnect();
	    });
	  }
	  get lastCommit() {
	    return this.#lastCommit;
	  }
	  observeRoot(target) {
	    this.#observedRoots.set(target, postNumberFromElement(target)), this.#observer.observe(target);
	    const cleanup = () => {
	      this.#observedRoots.delete(target), this.#deferredMeasurements.delete(target), this.#observer.unobserve(target);
	    };
	    return this.scope.add(cleanup);
	  }
	  notifyScroll() {
	    this.#schedule();
	  }
	  flushDeferredMeasurements() {
	    if (!this.#deferredMeasurements.size || this.scope.destroyed) return;
	    const entries = [...this.#deferredMeasurements.values()];
	    this.#deferredMeasurements.clear(), this.#recordMeasurements(entries, !0);
	  }
	  /** 同级投影(例如隐藏楼层分隔条)变化后主动刷新该根的物理占位。 */
	  refreshRootMeasurement(target) {
	    this.#observedRoots.has(target) && this.#recordMeasurements([Object.freeze({
	      target,
	      blockSize: target.getBoundingClientRect().height
	    })]);
	  }
	  flushNow() {
	    this.#frameHandle !== null && (this.#frames.cancel(this.#frameHandle), this.#frameHandle = null);
	    const inputBeforeCompensation = this.#readWindowInput();
	    if (this.#pendingCompensation !== 0) {
	      const compensation = this.#pendingCompensation, measurementScrollOffset = this.#measurementScrollOffset;
	      this.#clearMeasurementTransaction(), this.#shouldApplyScrollCompensation() && measurementScrollOffset !== null && Math.abs(inputBeforeCompensation.scrollOffset - measurementScrollOffset) <= SCROLL_OFFSET_EPSILON && this.#applyScrollCompensation(compensation);
	    } else
	      this.#measurementScrollOffset = null;
	    return this.#lastCommit = this.domController.commit(this.#readWindowInput()), this.#onCommit?.(this.#lastCommit), this.#lastCommit;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #recordMeasurements(entries, force = !1) {
	    if (!force && this.#shouldDeferMeasurements()) {
	      for (const entry of entries)
	        this.#deferredMeasurements.set(entry.target, entry);
	      this.#onMeasurementsDeferred();
	      return;
	    }
	    const input = this.#readWindowInput();
	    this.#measurementScrollOffset !== null && Math.abs(input.scrollOffset - this.#measurementScrollOffset) > SCROLL_OFFSET_EPSILON && this.#clearMeasurementTransaction();
	    const anchorPostNumber = this.domController.layout.window({
	      ...input,
	      overscanBeforeScreens: 0,
	      overscanAfterScreens: 0
	    }).postNumbers[0];
	    let changed = !1;
	    for (const entry of entries) {
	      const postNumber = postNumberFromElement(entry.target), blockSize = Math.round(
	        this.#resolveRootBlockSize(entry.target, entry.blockSize)
	      );
	      if (!this.#observedRoots.has(entry.target) || this.#observedRoots.get(entry.target) !== postNumber || postNumber === null || !Number.isFinite(blockSize) || blockSize <= 0) continue;
	      const result = this.domController.layout.measure(
	        postNumber,
	        blockSize,
	        anchorPostNumber
	      );
	      result.changed && (changed = !0, this.#measurementScrollOffset ??= input.scrollOffset, this.#pendingCompensation += result.scrollCompensation);
	    }
	    changed && this.#schedule();
	  }
	  #clearMeasurementTransaction() {
	    this.#pendingCompensation = 0, this.#measurementScrollOffset = null;
	  }
	  #schedule() {
	    this.scope.destroyed || this.#frameHandle !== null || (this.#frameHandle = this.#frames.request(() => {
	      this.#frameHandle = null, this.flushNow();
	    }));
	  }
	}
}, "cea160b0950ac7ebd97e7f8622aeb5e47f2fe9283617fce84143ff94c418a1d0");

/* Source: lite/src/stream/virtual-stream-view.ts */
runtime.register("src/stream/virtual-stream-view.js", function(module, exports, require) {
	var virtual_stream_view_exports = {};
	__export(virtual_stream_view_exports, {
	  VirtualStreamView: () => VirtualStreamView
	});
	module.exports = __toCommonJS(virtual_stream_view_exports);
	var import_html_element = require("../dom/html-element.js");
	function gapPlaceholder(document, side) {
	  const root = (0, import_html_element.htmlElement)(document, "div", "ldp-virtual-gap-placeholder"), avatar = (0, import_html_element.htmlElement)(document, "span", "ldp-virtual-gap-skeleton-avatar"), body = (0, import_html_element.htmlElement)(document, "span", "ldp-virtual-gap-skeleton-body"), line = (0, import_html_element.htmlElement)(document, "span", "ldp-virtual-gap-skeleton-line"), shortLine = (0, import_html_element.htmlElement)(
	    document,
	    "span",
	    "ldp-virtual-gap-skeleton-line ldp-virtual-gap-skeleton-line-short"
	  ), label = (0, import_html_element.htmlElement)(document, "span", "ldp-virtual-gap-label");
	  return avatar.setAttribute("aria-hidden", "true"), line.setAttribute("aria-hidden", "true"), shortLine.setAttribute("aria-hidden", "true"), label.textContent = "正在加载附近楼层…", body.append(line, shortLine, label), root.append(avatar, body), root.hidden = !0, root.setAttribute("data-gap-side", side), root.setAttribute("role", "status"), root.setAttribute("aria-live", "polite"), root;
	}
	class VirtualStreamView {
	  slots;
	  constructor(document) {
	    const root = (0, import_html_element.htmlElement)(document, "div", "ldp-virtual-stream"), beforeSpacer = (0, import_html_element.htmlElement)(document, "div", "ldp-virtual-spacer ldp-virtual-spacer-before"), beforeGapPlaceholder = gapPlaceholder(document, "before"), rootList = (0, import_html_element.htmlElement)(document, "div", "ldp-virtual-root-list"), afterSpacer = (0, import_html_element.htmlElement)(document, "div", "ldp-virtual-spacer ldp-virtual-spacer-after"), afterGapPlaceholder = gapPlaceholder(document, "after"), empty = (0, import_html_element.htmlElement)(document, "div", "ldp-comments-empty"), loadingTip = (0, import_html_element.htmlElement)(document, "div", "ldp-loading-tip"), loadingCopy = document.createElement("span"), endTip = (0, import_html_element.htmlElement)(document, "div", "ldp-end-tip");
	    beforeSpacer.setAttribute("aria-hidden", "true"), afterSpacer.setAttribute("aria-hidden", "true"), empty.textContent = "暂无评论", empty.hidden = !0, loadingCopy.textContent = "正在加载楼层…", loadingTip.append(loadingCopy), loadingTip.hidden = !0, loadingTip.setAttribute("role", "status"), loadingTip.setAttribute("aria-live", "polite"), endTip.textContent = "已经到底了~", endTip.hidden = !0, endTip.setAttribute("role", "status"), endTip.setAttribute("aria-live", "polite"), beforeSpacer.append(beforeGapPlaceholder), afterSpacer.append(afterGapPlaceholder), root.append(beforeSpacer, rootList, afterSpacer, empty, loadingTip, endTip), this.slots = Object.freeze({
	      root,
	      beforeSpacer,
	      beforeGapPlaceholder,
	      rootList,
	      afterSpacer,
	      afterGapPlaceholder,
	      empty,
	      loadingTip,
	      endTip
	    });
	  }
	  setGapPlaceholder(state) {
	    for (const [side, spacer, placeholder] of [
	      ["before", this.slots.beforeSpacer, this.slots.beforeGapPlaceholder],
	      ["after", this.slots.afterSpacer, this.slots.afterGapPlaceholder]
	    ]) {
	      const visible = state?.side === side;
	      if (placeholder.hidden = !visible, spacer.classList.toggle("has-gap-placeholder", visible), !visible) {
	        spacer.setAttribute("aria-hidden", "true"), placeholder.removeAttribute("data-target-post-number");
	        continue;
	      }
	      spacer.removeAttribute("aria-hidden"), placeholder.setAttribute(
	        "data-target-post-number",
	        String(state.targetPostNumber)
	      );
	    }
	  }
	  setSpacerSizes(before, after) {
	    const beforeSize = `${Math.max(0, before)}px`, afterSize = `${Math.max(0, after)}px`;
	    this.slots.beforeSpacer.style.blockSize !== beforeSize && (this.slots.beforeSpacer.style.blockSize = beforeSize), this.slots.afterSpacer.style.blockSize !== afterSize && (this.slots.afterSpacer.style.blockSize = afterSize);
	  }
	  setFlowState(state) {
	    this.slots.root.setAttribute("aria-busy", String(state.loading)), this.slots.loadingTip.hidden = !state.loading, this.slots.loadingTip.classList.toggle("show", state.loading), this.slots.endTip.hidden = !state.done, this.slots.endTip.classList.toggle("show", state.done), this.slots.empty.hidden = !state.empty;
	  }
	  destroy() {
	    this.slots.root.remove();
	  }
	}
}, "05ce613d0835acab8c277bfd8417391d40ad6795eb80edabc5f89676c3b19f5c");

/* Source: lite/src/topic/reader-native-topic-route.ts */
runtime.register("src/topic/reader-native-topic-route.js", function(module, exports, require) {
	var reader_native_topic_route_exports = {};
	__export(reader_native_topic_route_exports, {
	  READER_NATIVE_BYPASS_PARAMETER: () => READER_NATIVE_BYPASS_PARAMETER,
	  READER_NATIVE_BYPASS_TAB_KEY: () => READER_NATIVE_BYPASS_TAB_KEY,
	  consumeReaderNativeBypass: () => consumeReaderNativeBypass,
	  consumeReaderNativeTabBypass: () => consumeReaderNativeTabBypass,
	  openReaderNativeTopicTab: () => openReaderNativeTopicTab,
	  readerNativeBypassCleanHref: () => readerNativeBypassCleanHref,
	  readerNativeTopicHref: () => readerNativeTopicHref
	});
	module.exports = __toCommonJS(reader_native_topic_route_exports);
	const READER_NATIVE_BYPASS_PARAMETER = "ldp_native", READER_NATIVE_BYPASS_TAB_KEY = "linuxdo-enhanced-reader:native-tab";
	function httpUrl(value, baseValue) {
	  try {
	    const base = new URL(baseValue), url = new URL(value, base);
	    return /^https?:$/i.test(url.protocol) && url.origin === base.origin ? url : null;
	  } catch {
	    return null;
	  }
	}
	function readerNativeTopicHref(value, baseValue) {
	  const url = httpUrl(value, baseValue);
	  return url ? (url.searchParams.set(READER_NATIVE_BYPASS_PARAMETER, "1"), url.href) : "";
	}
	function readerNativeBypassCleanHref(value, baseValue) {
	  const url = httpUrl(value, baseValue);
	  return url?.searchParams.has(READER_NATIVE_BYPASS_PARAMETER) ? (url.searchParams.delete(READER_NATIVE_BYPASS_PARAMETER), url.href) : null;
	}
	function consumeReaderNativeBypass(value, baseValue, replace) {
	  const cleanHref = readerNativeBypassCleanHref(value, baseValue);
	  return cleanHref ? (replace(cleanHref), !0) : !1;
	}
	function consumeReaderNativeTabBypass(window) {
	  try {
	    const bypass = window.sessionStorage.getItem(
	      READER_NATIVE_BYPASS_TAB_KEY
	    ) === "1";
	    return bypass && window.sessionStorage.removeItem(READER_NATIVE_BYPASS_TAB_KEY), bypass;
	  } catch {
	    return !1;
	  }
	}
	function openReaderNativeTopicTab(window, value) {
	  let tab = null;
	  try {
	    tab = window.open("about:blank", "_blank");
	  } catch {
	    return !1;
	  }
	  if (!tab) return !1;
	  try {
	    tab.sessionStorage.setItem(READER_NATIVE_BYPASS_TAB_KEY, "1");
	  } catch {
	  }
	  try {
	    tab.opener = null;
	  } catch {
	  }
	  try {
	    tab.location.replace(value);
	  } catch {
	    tab.location.href = value;
	  }
	  return !0;
	}
}, "0f9505808f65e295e5a2963229477e4ad9daae1d1aa9a282907d558a769c35aa");

/* Source: lite/src/topic/reader-post-author-filter-feature.ts */
runtime.register("src/topic/reader-post-author-filter-feature.js", function(module, exports, require) {
	var reader_post_author_filter_feature_exports = {};
	__export(reader_post_author_filter_feature_exports, {
	  ReaderPostAuthorFilterFeature: () => ReaderPostAuthorFilterFeature
	});
	module.exports = __toCommonJS(reader_post_author_filter_feature_exports);
	var import_reader_unwanted_topic_filter = require("../collection/reader-unwanted-topic-filter.js");
	class ReaderPostAuthorFilterFeature {
	  activationScope = "node";
	  #views = /* @__PURE__ */ new Map();
	  #boundViews = /* @__PURE__ */ new WeakSet();
	  #preferences;
	  constructor(options) {
	    this.#preferences = options.preferences.read(), options.preferences.subscribe((preferences) => {
	      this.#preferences = preferences;
	      for (const [view, username] of this.#views)
	        this.#project(view, username);
	    }, options.parentScope), options.parentScope.add(() => this.#views.clear());
	  }
	  afterRender(_post, view) {
	    const username = view.identity.username;
	    this.#views.set(view, username), this.#boundViews.has(view) || (this.#boundViews.add(view), view.scope.add(() => this.#views.delete(view))), this.#project(view, username);
	  }
	  #project(view, username) {
	    const hidden = (0, import_reader_unwanted_topic_filter.readerUnwantedPostAuthorMatches)(
	      this.#preferences,
	      username
	    );
	    view.slots.root.classList.toggle("ldp-post-unwanted-author", hidden), hidden ? view.slots.root.dataset.unwantedPostAuthor = username : delete view.slots.root.dataset.unwantedPostAuthor;
	  }
	}
}, "201cc830587db6df721c9ddb84a7cf406ef8af913d07bb73161e5455f3d76c67");

/* Source: lite/src/topic/reader-post-presentation.ts */
runtime.register("src/topic/reader-post-presentation.js", function(module, exports, require) {
	var reader_post_presentation_exports = {};
	__export(reader_post_presentation_exports, {
	  createReaderPostPresentation: () => createReaderPostPresentation,
	  createReaderPostReadStateFeature: () => createReaderPostReadStateFeature
	});
	module.exports = __toCommonJS(reader_post_presentation_exports);
	var import_reader_image_fallback = require("../components/reader-image-fallback.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_topic_header = require("./reader-topic-header.js");
	const EMPTY_RECORD = Object.freeze({});
	function record(value) {
	  return value !== null && (typeof value == "object" || typeof value == "function") ? value : EMPTY_RECORD;
	}
	function text(value) {
	  return String(value ?? "").trim();
	}
	function positiveInteger(value, name) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return numeric;
	}
	function appendUserLink(document, parent, className, label, href, username) {
	  const link = document.createElement("a");
	  link.className = `ldp-user-link ${className}`, link.textContent = label, link.href = href || "#", link.target = "_blank", link.rel = "noopener", link.dataset.userCard = username, parent.append(link);
	}
	function appendAvatar(options, parent, post, username, displayName) {
	  const avatarTemplate = text(post.avatar_template), source = options.presentation.avatarSource(avatarTemplate, 48), exactSource = avatarTemplate.replace(/\{size\}/g, "48"), trigger = options.document.createElement("button");
	  if (trigger.type = "button", trigger.className = "ldp-user-link ldp-avatar-link", trigger.dataset.readerAvatar = "", trigger.dataset.userAvatarPreview = "", avatarTemplate && (trigger.dataset.userAvatarTemplate = avatarTemplate), trigger.dataset.userCard = username, trigger.setAttribute("aria-label", `查看 ${displayName} 的头像原图`), source) {
	    const avatar = options.document.createElement("img");
	    avatar.className = "ldp-avatar", avatar.alt = "", avatar.loading = "lazy", avatar.decoding = "async", trigger.append(avatar), (0, import_reader_image_fallback.installReaderImageSourceFallback)(avatar, [source, exactSource], () => {
	      const fallback = options.document.createElement("span");
	      return fallback.className = "ldp-avatar ldp-persistent-avatar-fallback", fallback.textContent = [...displayName || username || "?"][0] ?? "?", fallback.setAttribute("aria-hidden", "true"), fallback;
	    }, options.recoverAvatarSource, exactSource);
	  } else {
	    const fallback = options.document.createElement("span");
	    fallback.className = "ldp-avatar ldp-persistent-avatar-fallback", fallback.textContent = [...displayName || username || "?"][0] ?? "?", fallback.setAttribute("aria-hidden", "true"), trigger.append(fallback);
	  }
	  parent.append(trigger);
	}
	function appendBadge(document, parent, className, label) {
	  const badge = document.createElement("span");
	  badge.className = className, badge.textContent = label, parent.append(badge);
	}
	function appendFloor(document, parent, postNumber, _replyToPostNumber) {
	  const floor = document.createElement("span");
	  floor.className = "ldp-floor ldp-body-floor", floor.textContent = `#${postNumber}`, parent.append(floor);
	}
	function readStateIcon(document, read, renderIcon) {
	  let icon = read ? (0, import_reader_icon.renderReaderIcon)(document, "check", renderIcon) : null;
	  if (!icon) {
	    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
	    if (svg.setAttribute("viewBox", "0 0 24 24"), svg.setAttribute("aria-hidden", "true"), read) {
	      const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
	      path.setAttribute("d", "M5 12.5 9.5 17 19 7.5"), path.setAttribute("fill", "none"), path.setAttribute("stroke", "currentColor"), path.setAttribute("stroke-linecap", "round"), path.setAttribute("stroke-linejoin", "round"), svg.append(path);
	    } else {
	      const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
	      circle.setAttribute("cx", "12"), circle.setAttribute("cy", "12"), circle.setAttribute("r", "5.5"), svg.append(circle);
	    }
	    icon = svg;
	  }
	  return icon.nodeType === 1 && (icon.classList.add("ldp-icon", "ldp-post-read-state-icon"), icon.setAttribute("aria-hidden", "true")), icon;
	}
	function appendReadState(document, parent, read, renderIcon) {
	  const state = document.createElement("span");
	  state.className = "ldp-post-read-state", projectReadStateMarker(state, read, renderIcon), parent.append(state);
	}
	function projectReadStateMarker(state, read, renderIcon) {
	  const label = read ? "该楼层已读" : "该楼层未读";
	  state.classList.toggle("is-read", read), state.classList.toggle("is-unread", !read), state.dataset.readState = read ? "read" : "unread", state.dataset.ldpTooltipLabel = label, state.setAttribute("role", "img"), state.setAttribute("aria-label", label), state.replaceChildren(readStateIcon(state.ownerDocument, read, renderIcon));
	}
	function createReaderPostReadStateFeature(options) {
	  const scope = options.parentScope.child(), views = /* @__PURE__ */ new Map(), boundViews = /* @__PURE__ */ new WeakSet(), viewMarkers = /* @__PURE__ */ new WeakMap(), transitions = /* @__PURE__ */ new Map(), setTimer = options.setTimer ?? ((callback, milliseconds) => setTimeout(callback, milliseconds)), clearTimer = options.clearTimer ?? clearTimeout, isVisible = options.isVisible ?? ((view) => view.slots.root.isConnected && typeof view.slots.root.getClientRects == "function" && view.slots.root.getClientRects().length > 0), prefersReducedMotion = options.prefersReducedMotion ?? (() => !1), clearTransition = (marker) => {
	    const transition = transitions.get(marker);
	    transition && (clearTimer(transition.swapTimer), clearTimer(transition.finishTimer), marker.removeEventListener("animationend", transition.finish), transitions.delete(marker)), marker.classList.remove("is-confirming"), delete marker.dataset.readTransitionState;
	  }, syncViewReadState = (view, read, animate) => {
	    const marker = view.slots.header.querySelector(
	      ":scope > .ldp-post-read-state"
	    );
	    if (!marker) return;
	    const previousMarker = viewMarkers.get(view);
	    previousMarker && previousMarker !== marker && clearTransition(previousMarker), viewMarkers.set(view, marker);
	    const state = read ? "read" : "unread";
	    if (marker.dataset.readState === state || marker.dataset.readTransitionState === state) return;
	    if (clearTransition(marker), !animate || !read || !isVisible(view)) {
	      projectReadStateMarker(marker, read, options.renderIcon);
	      return;
	    }
	    marker.dataset.readTransitionState = state, marker.classList.add("is-confirming");
	    const finish = () => {
	      const transition = transitions.get(marker);
	      !transition || transition.finish !== finish || (clearTransition(marker), marker.dataset.readState !== state && projectReadStateMarker(marker, read, options.renderIcon));
	    };
	    marker.addEventListener("animationend", finish, { once: !0 });
	    const swapTimer = setTimer(() => {
	      const transition = transitions.get(marker);
	      !transition || transition.finish !== finish || projectReadStateMarker(marker, read, options.renderIcon);
	    }, 625), finishTimer = setTimer(finish, 1450);
	    transitions.set(marker, Object.freeze({
	      swapTimer,
	      finishTimer,
	      finish
	    }));
	  };
	  return options.readState.changes.subscribe((change) => {
	    for (const postNumber of change.postNumbers) {
	      const bindings = views.get(postNumber);
	      if (bindings)
	        for (const [view, sourceRead] of bindings)
	          syncViewReadState(
	            view,
	            sourceRead || options.readState.isOptimistic(postNumber),
	            change.kind === "optimistic" && !prefersReducedMotion()
	          );
	    }
	  }, scope), scope.add(() => {
	    for (const marker of transitions.keys()) clearTransition(marker);
	    views.clear();
	  }), Object.freeze({
	    afterRender(postValue, view) {
	      const post = record(postValue), postNumber = positiveInteger(
	        post.post_number,
	        "post.post_number"
	      ), bindings = views.get(postNumber) ?? /* @__PURE__ */ new Map();
	      bindings.set(view, post.read === !0), views.set(postNumber, bindings), boundViews.has(view) || (boundViews.add(view), view.scope.add(() => {
	        const marker = viewMarkers.get(view);
	        marker && clearTransition(marker), viewMarkers.delete(view);
	        const current = views.get(postNumber);
	        current?.delete(view), current?.size === 0 && views.delete(postNumber);
	      })), syncViewReadState(
	        view,
	        post.read === !0 || options.readState.isOptimistic(postNumber),
	        !1
	      );
	    }
	  });
	}
	function createReaderPostPresentation(options) {
	  const currentUsername = text(options.currentUsername), exactTimeByView = /* @__PURE__ */ new WeakMap();
	  return Object.freeze({
	    identity(postValue) {
	      const post = record(postValue);
	      return Object.freeze({
	        postId: positiveInteger(post.id, "post.id"),
	        postNumber: positiveInteger(
	          post.post_number,
	          "post.post_number"
	        ),
	        username: text(post.username),
	        createdAt: text(post.created_at)
	      });
	    },
	    render(postValue, view) {
	      const post = record(postValue), postNumber = positiveInteger(
	        post.post_number,
	        "post.post_number"
	      ), replyNumber = Number(post.reply_to_post_number), replyToPostNumber = Number.isSafeInteger(replyNumber) && replyNumber > 0 ? replyNumber : null, username = text(post.username), displayName = text(post.name) || username || "未知用户", profileHref = options.presentation.userHref(username), header = view.slots.header;
	      header.replaceChildren(), appendAvatar(options, header, post, username, displayName), appendUserLink(
	        options.document,
	        header,
	        "ldp-author",
	        displayName,
	        profileHref,
	        username
	      ), currentUsername && username === currentUsername && appendBadge(options.document, header, "ldp-me", "ME"), appendUserLink(
	        options.document,
	        header,
	        "ldp-user",
	        `@${username}`,
	        profileHref,
	        username
	      ), username && username === (0, import_reader_topic_header.readerTopicOwnerUsername)(options.readTopic()) && appendBadge(options.document, header, "ldp-op", "OP");
	      const createdAt = text(post.created_at) || text(view.identity.createdAt), relative = options.relativeTime(createdAt);
	      let exactLabel = null;
	      if (relative) {
	        const time = options.document.createElement("span");
	        time.className = "ldp-time";
	        const cachedExact = exactTimeByView.get(view), exact = cachedExact?.timestamp === createdAt ? cachedExact.value : options.exactTime(createdAt);
	        exact && cachedExact?.timestamp !== createdAt && exactTimeByView.set(view, Object.freeze({
	          timestamp: createdAt,
	          value: exact
	        })), exact && (time.dataset.exactTime = exact, exactLabel = options.document.createElement("span"), exactLabel.className = "ldp-time-exact", exactLabel.textContent = exact, exactLabel.setAttribute("aria-hidden", "true"));
	        const label = options.document.createElement("span");
	        label.className = "ldp-time-relative", label.textContent = `· ${relative}`, time.append(label), header.append(time);
	      }
	      post.hidden === !0 && appendBadge(
	        options.document,
	        header,
	        "ldp-special-badge ldp-hidden-badge warn",
	        "已隐藏"
	      ), appendFloor(
	        options.document,
	        header,
	        postNumber,
	        replyToPostNumber
	      ), exactLabel && header.append(exactLabel), appendReadState(
	        options.document,
	        header,
	        post.read === !0,
	        options.renderIcon
	      ), view.slots.content.innerHTML = text(post.cooked);
	    }
	  });
	}
}, "c8980b6989edde77ec490a513dd15f64967d1f58f21bb5921700ee70c145fdae");

/* Source: lite/src/topic/reader-post-view-projector.ts */
runtime.register("src/topic/reader-post-view-projector.js", function(module, exports, require) {
	var reader_post_view_projector_exports = {};
	__export(reader_post_view_projector_exports, {
	  ReaderPostViewProjector: () => ReaderPostViewProjector
	});
	module.exports = __toCommonJS(reader_post_view_projector_exports);
	var import_post_view = require("../dom/post-view.js");
	class ReaderPostViewProjector {
	  #document;
	  #identity;
	  #render;
	  #features;
	  #onError;
	  constructor(options) {
	    this.#document = options.document, this.#identity = options.identity, this.#render = options.render, this.#features = Object.freeze([...options.features ?? []]), this.#onError = options.onError ?? (() => {
	    });
	  }
	  identity(post) {
	    return this.#identity(post);
	  }
	  create(post, parentScope, expectedPostNumber) {
	    const view = this.createShell(post, parentScope, expectedPostNumber);
	    try {
	      return this.render(post, view), view;
	    } catch (error) {
	      throw view.destroy(), error;
	    }
	  }
	  /** 创建 canonical 固定槽位,但暂不解析 cooked 或投影 feature。 */
	  createShell(post, parentScope, expectedPostNumber) {
	    const view = new import_post_view.PostView(
	      this.#document,
	      this.#identity(post),
	      parentScope
	    );
	    if (expectedPostNumber !== void 0 && view.postNumber !== expectedPostNumber)
	      throw view.destroy(), new Error(
	        `PostView identity #${view.postNumber} 与 canonical 楼层 #${expectedPostNumber} 不一致`
	      );
	    return view;
	  }
	  render(post, view) {
	    for (const feature of this.#features)
	      try {
	        feature.beforeRender?.(post, view);
	      } catch (error) {
	        this.#onError(error);
	      }
	    try {
	      this.#render(post, view);
	    } finally {
	      for (const feature of this.#features)
	        try {
	          feature.afterRender?.(post, view);
	        } catch (error) {
	          this.#onError(error);
	        }
	    }
	  }
	  attach(root, postNumber, target = "all") {
	    this.#visitFeatures(target, (feature) => {
	      feature.attachRoot?.(root, postNumber);
	    });
	  }
	  detach(root, postNumber, target = "all") {
	    this.#visitFeatures(target, (feature) => {
	      feature.detachRoot?.(root, postNumber);
	    });
	  }
	  syncProjection() {
	    for (const feature of this.#features)
	      try {
	        feature.syncProjection?.();
	      } catch (error) {
	        this.#onError(error);
	      }
	  }
	  #visitFeatures(target, visit) {
	    for (const feature of this.#features) {
	      const scope = feature.activationScope ?? "branch";
	      if (!(target !== "all" && scope !== target))
	        try {
	          visit(feature);
	        } catch (error) {
	          this.#onError(error);
	        }
	    }
	  }
	}
}, "973fb472a06fa9ea020a5c3755fc8a12cb7682b791ffbb4bc0121490d7d6a10e");

/* Source: lite/src/topic/reader-reply-ancestor-resolver.ts */
runtime.register("src/topic/reader-reply-ancestor-resolver.js", function(module, exports, require) {
	var reader_reply_ancestor_resolver_exports = {};
	__export(reader_reply_ancestor_resolver_exports, {
	  resolveReaderReplyAncestors: () => resolveReaderReplyAncestors
	});
	module.exports = __toCommonJS(reader_reply_ancestor_resolver_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	async function resolveReaderReplyAncestors(session, targetPostNumberValue, options = {}) {
	  const targetPostNumber = (0, import_identifiers.discoursePostNumber)(targetPostNumberValue), stopBeforePostNumber = options.stopBeforePostNumber === void 0 ? null : (0, import_identifiers.discoursePostNumber)(options.stopBeforePostNumber), maxDepth = Math.max(1, Math.floor(options.maxDepth ?? 128)), isActive = options.isActive ?? (() => !0), seen = /* @__PURE__ */ new Set(), loadedPostNumbers = [];
	  let current = targetPostNumber, availableRoot = targetPostNumber;
	  for (let depth = 0; depth < maxDepth; depth += 1) {
	    if (!isActive())
	      return Object.freeze({
	        rootPostNumber: availableRoot,
	        complete: !1,
	        loadedPostNumbers: Object.freeze(loadedPostNumbers)
	      });
	    if (seen.has(current))
	      throw new Error(`楼层祖先链存在环,经过 #${current}`);
	    seen.add(current);
	    let post = session.postByNumber(current);
	    if (!post) {
	      try {
	        await session.loadTarget(current, {
	          scope: "single",
	          advanceCursor: !1
	        });
	      } catch (error) {
	        return Object.freeze({
	          rootPostNumber: availableRoot,
	          complete: !1,
	          loadedPostNumbers: Object.freeze(loadedPostNumbers),
	          ...isActive() ? { error } : {}
	        });
	      }
	      if (!isActive()) continue;
	      if (post = session.postByNumber(current), !post)
	        return Object.freeze({
	          rootPostNumber: availableRoot,
	          complete: !1,
	          loadedPostNumbers: Object.freeze(loadedPostNumbers)
	        });
	      loadedPostNumbers.push(current);
	    }
	    availableRoot = current;
	    const parent = (0, import_identifiers.discoursePostReference)(post).replyToPostNumber;
	    if (parent === null || parent === stopBeforePostNumber)
	      return Object.freeze({
	        rootPostNumber: current,
	        complete: !0,
	        loadedPostNumbers: Object.freeze(loadedPostNumbers)
	      });
	    current = (0, import_identifiers.discoursePostNumber)(parent);
	  }
	  return Object.freeze({
	    rootPostNumber: availableRoot,
	    complete: !1,
	    loadedPostNumbers: Object.freeze(loadedPostNumbers)
	  });
	}
}, "0e9a00d23cc3baf9951c000c767973a28696998cd9581061a576aa3590528d4e");

/* Source: lite/src/topic/reader-reply-tree-preferences.ts */
runtime.register("src/topic/reader-reply-tree-preferences.js", function(module, exports, require) {
	var reader_reply_tree_preferences_exports = {};
	__export(reader_reply_tree_preferences_exports, {
	  DEFAULT_READER_REPLY_TREE_PREFERENCES: () => DEFAULT_READER_REPLY_TREE_PREFERENCES,
	  ReaderReplyTreePreferencesPreview: () => ReaderReplyTreePreferencesPreview,
	  ReaderReplyTreePresentation: () => ReaderReplyTreePresentation,
	  normalizeReaderReplyTreePreferences: () => normalizeReaderReplyTreePreferences,
	  readerPreferencesReplyTreeAdapter: () => readerPreferencesReplyTreeAdapter
	});
	module.exports = __toCommonJS(reader_reply_tree_preferences_exports);
	var import_signal = require("../kernel/signal.js");
	const DEFAULT_READER_REPLY_TREE_PREFERENCES = Object.freeze({
	  expandNestedRepliesByDefault: !0,
	  expandLeafNestedReplies: !1,
	  aggregateDescendantReplies: !0,
	  inlineReplyTreeMaxDepth: 3,
	  hideNestedReplyFloors: !0
	});
	function normalizeReaderReplyTreePreferences(value) {
	  const expandLeafNestedReplies = value.expandLeafNestedReplies === !0, aggregateDescendantReplies = value.aggregateDescendantReplies === !0, expandNestedRepliesByDefault = aggregateDescendantReplies || value.expandNestedRepliesByDefault !== !1 || !expandLeafNestedReplies, rawDepth = Number(value.inlineReplyTreeMaxDepth);
	  return Object.freeze({
	    expandNestedRepliesByDefault,
	    expandLeafNestedReplies,
	    aggregateDescendantReplies: expandNestedRepliesByDefault && aggregateDescendantReplies,
	    inlineReplyTreeMaxDepth: Number.isFinite(rawDepth) ? Math.min(5, Math.max(1, Math.trunc(rawDepth))) : DEFAULT_READER_REPLY_TREE_PREFERENCES.inlineReplyTreeMaxDepth,
	    hideNestedReplyFloors: value.hideNestedReplyFloors === !0
	  });
	}
	class ReaderReplyTreePreferencesPreview {
	  changes = new import_signal.Signal();
	  #onError;
	  #value;
	  constructor(value, onError = () => {
	  }) {
	    this.#value = normalizeReaderReplyTreePreferences(value), this.#onError = onError;
	  }
	  read() {
	    return this.#value;
	  }
	  update(value) {
	    const next = normalizeReaderReplyTreePreferences(value);
	    if (!Object.keys(next).some((name) => !Object.is(next[name], this.#value[name]))) return !1;
	    this.#value = next;
	    for (const error of this.changes.emit(next)) this.#onError(error);
	    return !0;
	  }
	  subscribe(listener, scope) {
	    return this.changes.subscribe(listener, scope);
	  }
	  destroy() {
	    this.changes.clear();
	  }
	}
	const readerPreferencesReplyTreeAdapter = Object.freeze({
	  read: (preferences) => normalizeReaderReplyTreePreferences(preferences),
	  createPatch: (value) => normalizeReaderReplyTreePreferences(value)
	});
	function inlineDepth(preferences) {
	  return preferences.expandNestedRepliesByDefault ? preferences.aggregateDescendantReplies ? preferences.inlineReplyTreeMaxDepth : 1 : 0;
	}
	class ReaderReplyTreePresentation {
	  canonical;
	  #frozenCanonical = null;
	  #frozenCanonicalSourceRevision = -1;
	  #frozenCanonicalCoverageComplete = !0;
	  #frozenCanonicalPostStreamRevision = 0;
	  #preferences;
	  #canonicalCoverageComplete;
	  #canonicalPostStreamRevision;
	  #canonicalPostStreamGapCount;
	  #revealedFloors = /* @__PURE__ */ new Set();
	  #revealedParents = /* @__PURE__ */ new Set();
	  #degradedFloorRoots = /* @__PURE__ */ new Set();
	  #degradedFloorCanonicalRevision = -1;
	  #postFilter = null;
	  #projectionRevision = 0;
	  #cachedCanonicalRevision = -1;
	  #cachedCanonicalCoverageComplete = null;
	  #cachedCanonicalPostStreamRevision = -1;
	  #cachedProjectionRevision = -1;
	  #trustedCanonicalCoverageThroughPostNumber = 0;
	  #cachedRoots = Object.freeze([]);
	  #cachedRootBranches = Object.freeze([]);
	  #cachedSubtreePostCounts = /* @__PURE__ */ new Map();
	  #cachedHiddenFloorRunsAfter = /* @__PURE__ */ new Map();
	  constructor(canonical, preferences = DEFAULT_READER_REPLY_TREE_PREFERENCES, options = {}) {
	    this.canonical = canonical, this.#preferences = normalizeReaderReplyTreePreferences(preferences), this.#canonicalCoverageComplete = options.canonicalCoverageComplete ?? (() => !0), this.#canonicalPostStreamRevision = options.canonicalPostStreamRevision ?? (() => 0), this.#canonicalPostStreamGapCount = options.canonicalPostStreamGapCount;
	  }
	  get preferences() {
	    return this.#preferences;
	  }
	  get revision() {
	    return `${this.#activeCanonicalRevision()}:${Number(this.#activeCanonicalCoverageComplete())}:${this.#activeCanonicalPostStreamRevision()}:${this.#projectionRevision}`;
	  }
	  get postFilterKey() {
	    return this.#postFilter?.key ?? null;
	  }
	  postFilterMatches(postNumber) {
	    return this.#postFilter?.matches(postNumber) ?? !0;
	  }
	  /**
	   * 用户连续滚动期间固定一次只读关系快照。
	   *
	   * canonical 仍可被 MessageBus 即时更新并持久化;虚拟窗口、DOM owner 和回复线只读
	   * 这个短生命周期投影,避免同一个滚轮手势中途改变根高或父子挂载。重复调用不会复制。
	   */
	  freezeCanonical() {
	    return this.#frozenCanonical ? !1 : (this.#frozenCanonical = this.canonical.clone(), this.#frozenCanonicalSourceRevision = this.canonical.revision, this.#frozenCanonicalCoverageComplete = this.#canonicalCoverageComplete(), this.#frozenCanonicalPostStreamRevision = this.#canonicalPostStreamRevision(), !0);
	  }
	  /** 停滚后切回最新 canonical;返回冻结期间关系是否发生变化。 */
	  thawCanonical() {
	    if (!this.#frozenCanonical) return !1;
	    const changed = this.#frozenCanonicalSourceRevision !== this.canonical.revision || this.#frozenCanonicalCoverageComplete !== this.#canonicalCoverageComplete() || this.#frozenCanonicalPostStreamRevision !== this.#canonicalPostStreamRevision();
	    return this.#frozenCanonical = null, this.#frozenCanonicalSourceRevision = -1, this.#frozenCanonicalCoverageComplete = !0, this.#frozenCanonicalPostStreamRevision = 0, changed;
	  }
	  get canonicalFrozen() {
	    return this.#frozenCanonical !== null;
	  }
	  /** 与当前活动投影同源;滚动冻结期间返回冻结时的覆盖状态。 */
	  get coverageComplete() {
	    return this.#activeCanonicalCoverageComplete();
	  }
	  update(preferences) {
	    const next = normalizeReaderReplyTreePreferences(preferences), changed = Object.keys(next).some((name) => !Object.is(next[name], this.#preferences[name]));
	    return changed && (this.#preferences = next, this.#revealedFloors.clear(), this.#revealedParents.clear(), this.#degradedFloorRoots.clear(), this.#degradedFloorCanonicalRevision = -1, this.#projectionRevision += 1), changed;
	  }
	  setPostFilter(filter) {
	    const nextKey = filter?.key ?? "", currentKey = this.#postFilter?.key ?? "";
	    return nextKey === currentKey ? !1 : (this.#postFilter = filter, this.#revealedFloors.clear(), this.#revealedParents.clear(), this.#degradedFloorRoots.clear(), this.#degradedFloorCanonicalRevision = -1, this.#projectionRevision += 1, !0);
	  }
	  invalidatePostFilter() {
	    return this.#postFilter ? (this.#projectionRevision += 1, !0) : !1;
	  }
	  revealAsFloor(postNumber) {
	    return !this.#activeCanonical().has(postNumber) || this.rootOf(postNumber) !== void 0 ? !1 : (this.#postFilter && this.#revealedFloors.clear(), this.#revealedFloors.add(postNumber), this.#projectionRevision += 1, !0);
	  }
	  /**
	   * canonical 父链在某个不可读楼层处断开时,把最高可用祖先投影成临时根,并沿已确认
	   * 的真实 parent 关系揭示到目标。只修改短生命周期显示投影,不伪造 canonical 关系。
	   */
	  revealDegradedBranch(rootPostNumber, targetPostNumber) {
	    const canonical = this.#activeCanonical();
	    if (!canonical.has(rootPostNumber) || !canonical.has(targetPostNumber) || canonical.depthOf(rootPostNumber) !== void 0)
	      return !1;
	    const parents = [], seen = /* @__PURE__ */ new Set([targetPostNumber]);
	    let current = targetPostNumber;
	    for (; current !== rootPostNumber; ) {
	      const parent = canonical.parentOf(current);
	      if (parent == null || seen.has(parent))
	        return !1;
	      parents.push(parent), seen.add(parent), current = parent;
	    }
	    let changed = !1;
	    this.#degradedFloorRoots.has(rootPostNumber) || (this.#degradedFloorRoots.add(rootPostNumber), changed = !0), this.#revealedFloors.has(rootPostNumber) || (this.#revealedFloors.add(rootPostNumber), changed = !0);
	    for (const parent of parents)
	      parent === targetPostNumber || this.#revealedParents.has(parent) || (this.#revealedParents.add(parent), changed = !0);
	    return this.#degradedFloorCanonicalRevision = this.#activeCanonicalRevision(), changed && (this.#projectionRevision += 1), changed;
	  }
	  parentOf(postNumber) {
	    const canonical = this.#activeCanonical(), canonicalDepth = canonical.depthOf(postNumber), degradedPath = this.#degradedFloorPath(postNumber, canonical), projectedDepth = degradedPath?.depth ?? canonicalDepth;
	    if (projectedDepth === void 0)
	      return this.#postFilter && this.#matchesPostFilter(postNumber) ? null : void 0;
	    if (degradedPath?.depth === 0 || this.#isRevealedFloor(postNumber, canonicalDepth)) return null;
	    if (this.#matchesPostFilter(postNumber))
	      return this.#postFilter || projectedDepth === 0 ? null : this.#isInlineVisible(postNumber, projectedDepth) ? canonical.parentOf(postNumber) : this.#showAsFloor() ? null : void 0;
	  }
	  childrenOf(postNumber) {
	    const canonical = this.#activeCanonical();
	    if (this.#postFilter) return Object.freeze([]);
	    const parentDepth = this.#projectedDepth(postNumber, canonical);
	    return parentDepth === void 0 || !this.#isInlineVisible(postNumber, parentDepth) || parentDepth >= inlineDepth(this.#preferences) && !this.#revealedParents.has(postNumber) ? Object.freeze([]) : canonical.childrenOf(postNumber);
	  }
	  subtreePostCountOf(postNumber) {
	    this.#syncCache();
	    const cached = this.#cachedSubtreePostCounts.get(postNumber);
	    if (cached !== void 0) return cached;
	    if (this.parentOf(postNumber) === void 0) return;
	    const childrenByPost = /* @__PURE__ */ new Map(), order = [], pending = [postNumber], visited = /* @__PURE__ */ new Set();
	    for (; pending.length; ) {
	      const current = pending.pop();
	      if (visited.has(current)) continue;
	      visited.add(current), order.push(current);
	      const children = this.childrenOf(current);
	      childrenByPost.set(current, children), pending.push(...children);
	    }
	    const subtreePostCounts = /* @__PURE__ */ new Map();
	    for (let index = order.length - 1; index >= 0; index -= 1) {
	      const current = order[index], subtreePostCount = 1 + (childrenByPost.get(current) ?? []).reduce(
	        (count, childPostNumber) => count + (subtreePostCounts.get(childPostNumber) ?? 1),
	        0
	      );
	      subtreePostCounts.set(current, subtreePostCount), subtreePostCount > 1 && this.#cachedSubtreePostCounts.set(current, subtreePostCount);
	    }
	    return subtreePostCounts.get(postNumber) ?? 1;
	  }
	  hiddenDirectChildrenOf(postNumber) {
	    if (this.#postFilter) return Object.freeze([]);
	    const canonical = this.#activeCanonical(), visibleChildren = new Set(this.childrenOf(postNumber));
	    return Object.freeze(
	      canonical.childrenOf(postNumber).filter((childPostNumber) => !visibleChildren.has(childPostNumber) && this.rootOf(childPostNumber) === void 0)
	    );
	  }
	  /** 用户只揭示当前父节点的直属下一层;更深后代仍停在各自的“+”之后。 */
	  revealNextLevel(postNumber) {
	    if (this.#postFilter) return !1;
	    const canonical = this.#activeCanonical(), projectedDepth = this.#projectedDepth(postNumber, canonical);
	    return projectedDepth === void 0 || !this.#isInlineVisible(postNumber, projectedDepth) || this.#revealedParents.has(postNumber) || this.hiddenDirectChildrenOf(postNumber).length === 0 ? !1 : (this.#revealedParents.add(postNumber), this.#projectionRevision += 1, !0);
	  }
	  hiddenFloorRunAfter(postNumber) {
	    return this.#syncCache(), this.#cachedHiddenFloorRunsAfter.get(postNumber) ?? Object.freeze([]);
	  }
	  depthOf(postNumber) {
	    const canonical = this.#activeCanonical(), canonicalDepth = canonical.depthOf(postNumber), degradedPath = this.#degradedFloorPath(postNumber, canonical), projectedDepth = degradedPath?.depth ?? canonicalDepth;
	    if (projectedDepth === void 0)
	      return this.#postFilter && this.#matchesPostFilter(postNumber) ? 0 : void 0;
	    if (degradedPath?.depth === 0 || this.#isRevealedFloor(postNumber, canonicalDepth)) return 0;
	    if (this.#matchesPostFilter(postNumber))
	      return this.#postFilter ? 0 : this.#isInlineVisible(postNumber, projectedDepth) ? projectedDepth : this.#showAsFloor() ? 0 : void 0;
	  }
	  rootOf(postNumber) {
	    const canonical = this.#activeCanonical(), canonicalDepth = canonical.depthOf(postNumber), degradedPath = this.#degradedFloorPath(postNumber, canonical), projectedDepth = degradedPath?.depth ?? canonicalDepth;
	    if (projectedDepth === void 0)
	      return this.#postFilter && this.#matchesPostFilter(postNumber) ? postNumber : void 0;
	    if (degradedPath?.depth === 0 || this.#isRevealedFloor(postNumber, canonicalDepth)) return postNumber;
	    if (this.#matchesPostFilter(postNumber))
	      return this.#postFilter ? postNumber : this.#isInlineVisible(postNumber, projectedDepth) ? degradedPath?.rootPostNumber ?? canonical.rootOf(postNumber) : this.#showAsFloor() ? postNumber : void 0;
	  }
	  roots() {
	    return this.#syncCache(), this.#cachedRoots;
	  }
	  rootBranches() {
	    return this.#syncCache(), this.#cachedRootBranches;
	  }
	  #syncCache() {
	    const canonical = this.#activeCanonical(), canonicalRevision = this.#activeCanonicalRevision(), canonicalCoverageComplete = this.#activeCanonicalCoverageComplete(), canonicalPostStreamRevision = this.#activeCanonicalPostStreamRevision();
	    if (this.#cachedCanonicalRevision === canonicalRevision && this.#cachedCanonicalCoverageComplete === canonicalCoverageComplete && this.#cachedCanonicalPostStreamRevision === canonicalPostStreamRevision && this.#cachedProjectionRevision === this.#projectionRevision) return;
	    const postNumbers = canonical.postNumbers();
	    canonicalCoverageComplete && (this.#trustedCanonicalCoverageThroughPostNumber = Math.max(
	      this.#trustedCanonicalCoverageThroughPostNumber,
	      postNumbers.at(-1) ?? 0
	    ));
	    const trustedCoverageThroughPostNumber = this.#trustedCanonicalCoverageThroughPostNumber, roots = Object.freeze(postNumbers.filter((postNumber) => this.parentOf(postNumber) === null).sort((left, right) => left - right)), subtreePostCounts = /* @__PURE__ */ new Map();
	    let canonicalIndex = 0, previousRootCanonicalIndex = -1, previousRootPostNumber = 0;
	    const rootBranches = Object.freeze(roots.map((postNumber) => {
	      for (; canonicalIndex < postNumbers.length && postNumbers[canonicalIndex] < postNumber; ) canonicalIndex += 1;
	      const previousKnownPostNumber = canonicalIndex > 0 ? postNumbers[canonicalIndex - 1] ?? 0 : 0, canonicalPostStreamGapCount = this.#canonicalPostStreamGapCount?.(
	        postNumber,
	        previousRootPostNumber
	      ), knownCanonicalPostCountBetween = Math.max(
	        0,
	        canonicalIndex - previousRootCanonicalIndex - 1
	      ), exactUnloadedPostCountBefore = canonicalPostStreamGapCount === void 0 ? void 0 : Number.isSafeInteger(canonicalPostStreamGapCount) && canonicalPostStreamGapCount >= 0 ? Math.max(
	        0,
	        canonicalPostStreamGapCount - knownCanonicalPostCountBetween
	      ) : 0, unloadedPostCountBefore = !canonicalCoverageComplete && !this.#postFilter ? postNumber <= trustedCoverageThroughPostNumber ? 0 : exactUnloadedPostCountBefore === void 0 ? Math.max(
	        0,
	        postNumber - Math.max(
	          previousKnownPostNumber,
	          trustedCoverageThroughPostNumber
	        ) - 1
	      ) : exactUnloadedPostCountBefore : 0;
	      previousRootCanonicalIndex = canonicalIndex, previousRootPostNumber = postNumber;
	      const pending = [postNumber], visited = /* @__PURE__ */ new Set();
	      let subtreePostCount = 0;
	      for (; pending.length; ) {
	        const current = pending.pop();
	        visited.has(current) || (visited.add(current), subtreePostCount += 1, pending.push(...this.childrenOf(current)));
	      }
	      return subtreePostCount > 1 && subtreePostCounts.set(postNumber, subtreePostCount), Object.freeze({
	        postNumber,
	        subtreePostCount,
	        ...unloadedPostCountBefore > 0 ? { unloadedPostCountBefore } : {}
	      });
	    })), hiddenFloorRunsAfter = /* @__PURE__ */ new Map();
	    if (!this.#postFilter && this.#preferences.hideNestedReplyFloors) {
	      let relationIndex = 0;
	      for (let index = 0; index < roots.length; index += 1) {
	        const rootPostNumber = roots[index], nextRootPostNumber = roots[index + 1] ?? Number.POSITIVE_INFINITY;
	        for (; relationIndex < postNumbers.length && postNumbers[relationIndex] <= rootPostNumber; )
	          relationIndex += 1;
	        const run = [];
	        let previousPostNumber = rootPostNumber;
	        for (; relationIndex < postNumbers.length && postNumbers[relationIndex] < nextRootPostNumber; ) {
	          const hiddenPostNumber = postNumbers[relationIndex];
	          if (!canonicalCoverageComplete && hiddenPostNumber > trustedCoverageThroughPostNumber && hiddenPostNumber !== previousPostNumber + 1) break;
	          run.push(hiddenPostNumber), previousPostNumber = hiddenPostNumber, relationIndex += 1;
	        }
	        run.length > 0 && hiddenFloorRunsAfter.set(
	          rootPostNumber,
	          Object.freeze(run)
	        );
	      }
	    }
	    this.#cachedRoots = roots, this.#cachedRootBranches = rootBranches, this.#cachedSubtreePostCounts = subtreePostCounts, this.#cachedHiddenFloorRunsAfter = hiddenFloorRunsAfter, this.#cachedCanonicalRevision = canonicalRevision, this.#cachedCanonicalCoverageComplete = canonicalCoverageComplete, this.#cachedCanonicalPostStreamRevision = canonicalPostStreamRevision, this.#cachedProjectionRevision = this.#projectionRevision;
	  }
	  #activeCanonical() {
	    return this.#frozenCanonical ?? this.canonical;
	  }
	  #activeCanonicalRevision() {
	    return this.#frozenCanonical ? this.#frozenCanonicalSourceRevision : this.canonical.revision;
	  }
	  #activeCanonicalCoverageComplete() {
	    return this.#frozenCanonical ? this.#frozenCanonicalCoverageComplete : this.#canonicalCoverageComplete();
	  }
	  #activeCanonicalPostStreamRevision() {
	    return this.#frozenCanonical ? this.#frozenCanonicalPostStreamRevision : this.#canonicalPostStreamRevision();
	  }
	  #showAsFloor() {
	    return this.#preferences.expandLeafNestedReplies || !this.#preferences.hideNestedReplyFloors;
	  }
	  #syncDegradedFloorRoots(canonical) {
	    const revision = this.#activeCanonicalRevision();
	    if (revision !== this.#degradedFloorCanonicalRevision) {
	      for (const rootPostNumber of [...this.#degradedFloorRoots])
	        canonical.depthOf(rootPostNumber) !== void 0 && (this.#degradedFloorRoots.delete(rootPostNumber), this.#revealedFloors.delete(rootPostNumber));
	      this.#degradedFloorCanonicalRevision = revision;
	    }
	  }
	  #degradedFloorPath(postNumber, canonical) {
	    if (this.#syncDegradedFloorRoots(canonical), !this.#degradedFloorRoots.size || !canonical.has(postNumber))
	      return;
	    const seen = /* @__PURE__ */ new Set();
	    let current = postNumber, depth = 0;
	    for (; !seen.has(current); ) {
	      if (this.#degradedFloorRoots.has(current))
	        return Object.freeze({ rootPostNumber: current, depth });
	      seen.add(current);
	      const parent = canonical.parentOf(current);
	      if (parent == null) return;
	      current = parent, depth += 1;
	    }
	  }
	  #projectedDepth(postNumber, canonical) {
	    return this.#degradedFloorPath(postNumber, canonical)?.depth ?? canonical.depthOf(postNumber);
	  }
	  #isRevealedFloor(postNumber, canonicalDepth) {
	    return this.#degradedFloorRoots.has(postNumber) ? !0 : !this.#revealedFloors.has(postNumber) || canonicalDepth === void 0 ? !1 : this.#postFilter && !this.#matchesPostFilter(postNumber) || canonicalDepth > inlineDepth(this.#preferences) ? !0 : (this.#revealedFloors.delete(postNumber), !1);
	  }
	  #isInlineVisible(postNumber, projectedDepth) {
	    if (projectedDepth <= inlineDepth(this.#preferences)) return !0;
	    const canonical = this.#activeCanonical(), parentPostNumber = canonical.parentOf(postNumber);
	    if (parentPostNumber == null || !this.#revealedParents.has(parentPostNumber)) return !1;
	    const parentDepth = this.#projectedDepth(parentPostNumber, canonical);
	    return parentDepth !== void 0 && this.#isInlineVisible(parentPostNumber, parentDepth);
	  }
	  #matchesPostFilter(postNumber) {
	    const filter = this.#postFilter;
	    if (!filter) return !0;
	    if (!filter.matches(postNumber)) return !1;
	    if (!filter.hideDescendantMatches) return !0;
	    const canonical = this.#activeCanonical(), seen = /* @__PURE__ */ new Set([postNumber]);
	    let parent = canonical.parentOf(postNumber);
	    for (; parent != null && !seen.has(parent); ) {
	      if (parent === filter.ancestorBoundaryPostNumber) return !0;
	      if (filter.matches(parent)) return !1;
	      seen.add(parent), parent = canonical.parentOf(parent);
	    }
	    return !0;
	  }
	}
}, "3a7c25e5877aee489ff7dab29ad1d03825ba400a54f95167c3a96a5b026049c0");

/* Source: lite/src/topic/reader-topic-comments-header.ts */
runtime.register("src/topic/reader-topic-comments-header.js", function(module, exports, require) {
	var reader_topic_comments_header_exports = {};
	__export(reader_topic_comments_header_exports, {
	  ReaderTopicCommentsHeader: () => ReaderTopicCommentsHeader
	});
	module.exports = __toCommonJS(reader_topic_comments_header_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_value_record = require("../kernel/value-record.js");
	function positiveCount(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : 0;
	}
	function commentCount(topicValue, posts) {
	  const topic = (0, import_value_record.objectRecord)(topicValue), total = Math.max(
	    positiveCount(topic?.posts_count),
	    positiveCount(topic?.highest_post_number),
	    posts.reduce(
	      (maximum, post) => Math.max(maximum, positiveCount(post.post_number)),
	      0
	    )
	  );
	  return Math.max(0, total - 1);
	}
	class ReaderTopicCommentsHeader {
	  scope;
	  #document;
	  #session;
	  #presentation;
	  #currentUsername;
	  #renderIcon;
	  #onError;
	  #count = null;
	  #presence = null;
	  #presenceUsers = Object.freeze([]);
	  constructor(options) {
	    this.#document = options.document, this.#session = options.session, this.#presentation = options.presentation, this.#currentUsername = String(options.currentUsername ?? "").trim(), this.#renderIcon = options.renderIcon, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#session.changes.subscribe(() => this.#refreshCount(), this.scope), this.scope.add(options.presence.watchReplying(
	      options.topicId,
	      (users) => {
	        this.#presenceUsers = Object.freeze(users.filter(
	          (user) => user.username !== this.#currentUsername
	        )), this.#renderPresence();
	      },
	      this.#onError
	    )), this.scope.add(() => {
	      this.#count = null, this.#presence = null;
	    });
	  }
	  afterRender(post, view) {
	    if (positiveCount(post.post_number) !== 1) return;
	    let header = view.slots.root.querySelector(
	      ":scope > .ldp-comments-header"
	    );
	    header || (header = this.#createHeader(), view.slots.replyTree.after(header)), this.#count = header.querySelector(
	      ":scope > .ldp-comments-count"
	    ), this.#presence = header.querySelector(
	      ":scope > .ldp-topic-presence"
	    ), this.#refreshCount(), this.#renderPresence();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #createHeader() {
	    const header = this.#document.createElement("div");
	    header.className = "ldp-comments-header", header.append((0, import_reader_icon.renderReaderIcon)(
	      this.#document,
	      "message-square",
	      this.#renderIcon
	    ));
	    const label = this.#document.createElement("span");
	    label.className = "ldp-comments-label", label.textContent = "评论";
	    const count = this.#document.createElement("span");
	    count.className = "ldp-comments-count", count.setAttribute("aria-live", "polite");
	    const presence = this.#document.createElement("span");
	    return presence.className = "ldp-topic-presence", presence.hidden = !0, presence.setAttribute("aria-live", "polite"), header.append(label, count, presence), header;
	  }
	  #refreshCount() {
	    if (!this.#count) return;
	    const count = commentCount(
	      this.#session.topic,
	      this.#session.cachedPosts()
	    );
	    this.#count.textContent = `(${count})`;
	  }
	  #renderPresence() {
	    const root = this.#presence;
	    if (!root?.isConnected) return;
	    root.replaceChildren();
	    const users = this.#presenceUsers;
	    if (!users.length) {
	      root.hidden = !0;
	      return;
	    }
	    const avatars = this.#document.createElement("span");
	    avatars.className = "ldp-topic-presence-avatars";
	    for (const user of users.slice(0, 3)) {
	      const link = this.#document.createElement("a");
	      link.className = "ldp-topic-presence-user ldp-user-link", link.href = this.#presentation.userHref(user.username), link.target = "_blank", link.rel = "noopener noreferrer", link.dataset.userCard = user.username, link.setAttribute("aria-label", user.name);
	      const source = user.avatarTemplate ? this.#presentation.avatarSource(user.avatarTemplate, 24) : "";
	      if (source) {
	        const image = this.#document.createElement("img");
	        image.className = "ldp-topic-presence-avatar", image.src = source, image.alt = "", image.loading = "lazy", image.decoding = "async", link.append(image);
	      } else {
	        const fallback = this.#document.createElement("span");
	        fallback.className = "ldp-topic-presence-avatar ldp-avatar-fallback", fallback.textContent = user.name.slice(0, 1).toUpperCase(), link.append(fallback);
	      }
	      avatars.append(link);
	    }
	    const label = this.#document.createElement("span");
	    label.className = "ldp-topic-presence-text", label.textContent = users.length === 1 ? `${users[0].name} 正在回复…` : `${users.length} 人正在回复…`, root.append(avatars, label), root.hidden = !1;
	  }
	}
}, "8b1bf40506b52a9ea96a53da13c90de849ed71a3a90e138944b1b6618dc1c90d");

/* Source: lite/src/topic/reader-topic-context-controller.ts */
runtime.register("src/topic/reader-topic-context-controller.js", function(module, exports, require) {
	var reader_topic_context_controller_exports = {};
	__export(reader_topic_context_controller_exports, {
	  ReaderTopicContextController: () => ReaderTopicContextController
	});
	module.exports = __toCommonJS(reader_topic_context_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_reply_ancestor_resolver = require("./reader-reply-ancestor-resolver.js");
	function frozenPostNumbers(values) {
	  return Object.freeze([...values].sort((left, right) => left - right));
	}
	class ReaderTopicContextController {
	  scope;
	  changes = new import_signal.Signal();
	  #session;
	  #replies;
	  #loadCrossTopicQuotedPost;
	  #onError;
	  #quotedPosts = /* @__PURE__ */ new Map();
	  #unavailableQuotedPostKeys = /* @__PURE__ */ new Set();
	  #collapsedPostNumbers = /* @__PURE__ */ new Set();
	  #discussionContextualParents = /* @__PURE__ */ new Map();
	  #discussionRootPostNumber = null;
	  #discussionDescendantRootPostNumber = null;
	  #discussionTargetPostNumber = null;
	  #discussionLoading = !1;
	  #discussionBranchPartial = !1;
	  #discussionUsesGlobalCoverage = !1;
	  #revision = 0;
	  #discussionEpoch = 0;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#session = options.session, this.#replies = options.replies, this.#loadCrossTopicQuotedPost = options.loadCrossTopicQuotedPost, this.#onError = options.onError ?? (() => {
	    }), this.#session.changes.subscribe(() => this.#emit(), this.scope), this.#replies.changes.subscribe(() => this.#emit(), this.scope), this.scope.add(() => {
	      this.#discussionEpoch += 1, this.#collapsedPostNumbers.clear(), this.#discussionContextualParents.clear(), this.#quotedPosts.clear(), this.#unavailableQuotedPostKeys.clear(), this.changes.clear();
	    });
	  }
	  get topicId() {
	    return (0, import_identifiers.discourseTopicId)(this.#session.topicId);
	  }
	  postByNumber(postNumberValue) {
	    return this.#session.postByNumber(
	      (0, import_identifiers.discoursePostNumber)(postNumberValue)
	    );
	  }
	  quotedPost(topicIdValue, postNumberValue) {
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue);
	    return topicId === this.topicId ? this.#session.postByNumber(postNumber) : this.#quotedPosts.get(`${topicId}:${postNumber}`);
	  }
	  snapshot() {
	    return Object.freeze({
	      discussion: this.#discussionSnapshot(),
	      revision: this.#revision
	    });
	  }
	  /** 同 Topic 复用 canonical session;跨 Topic 通过组合根注入的统一请求网关读取。 */
	  async loadQuotedPost(topicIdValue, postNumberValue) {
	    this.#assertActive();
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue), key = `${topicId}:${postNumber}`;
	    if (topicId !== this.topicId) {
	      const cached2 = this.#quotedPosts.get(key);
	      if (cached2) return cached2;
	      if (this.#unavailableQuotedPostKeys.has(key) || !this.#loadCrossTopicQuotedPost) return null;
	      try {
	        const post = await this.#loadCrossTopicQuotedPost(
	          topicId,
	          postNumber
	        );
	        return this.scope.destroyed ? null : post ? (this.#unavailableQuotedPostKeys.delete(key), this.#quotedPosts.set(key, post), post) : (this.#unavailableQuotedPostKeys.add(key), null);
	      } catch (error) {
	        return this.scope.destroyed || this.#onError(error), null;
	      }
	    }
	    const cached = this.#session.postByNumber(postNumber);
	    if (cached) return cached;
	    try {
	      await this.#session.loadTarget(postNumber, {
	        scope: "single",
	        advanceCursor: !1
	      });
	    } catch (error) {
	      return this.scope.destroyed || this.#onError(error), null;
	    }
	    return this.scope.destroyed ? null : this.#session.postByNumber(postNumber) ?? null;
	  }
	  async openDiscussion(postNumberValue, options = {}) {
	    this.#assertActive();
	    const requestedPostNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue), targetPostNumber = options.targetPostNumber === null ? null : (0, import_identifiers.discoursePostNumber)(
	      options.targetPostNumber ?? requestedPostNumber
	    ), requestedDescendantRootPostNumber = options.descendantRootPostNumber === void 0 ? null : (0, import_identifiers.discoursePostNumber)(options.descendantRootPostNumber), epoch = ++this.#discussionEpoch;
	    this.#discussionLoading = !0, this.#discussionBranchPartial = !1, this.#discussionUsesGlobalCoverage = !1, this.#collapsedPostNumbers.clear(), this.#discussionContextualParents.clear();
	    let rootPostNumber = requestedPostNumber;
	    try {
	      await this.#ensurePost(requestedPostNumber), requestedDescendantRootPostNumber !== null && requestedDescendantRootPostNumber !== requestedPostNumber && await this.#ensurePost(requestedDescendantRootPostNumber), options.explicitRoot || (rootPostNumber = await this.#branchRootOf(
	        requestedPostNumber,
	        epoch
	      ));
	    } catch (error) {
	      epoch === this.#discussionEpoch && !this.scope.destroyed && (this.#discussionBranchPartial = !0, this.#onError(error));
	    }
	    if (epoch !== this.#discussionEpoch || this.scope.destroyed)
	      return this.snapshot();
	    if (!this.#session.postByNumber(rootPostNumber))
	      return this.#discussionLoading = !1, this.#discussionRootPostNumber = null, this.#discussionDescendantRootPostNumber = null, this.#discussionTargetPostNumber = null, this.#emit();
	    const descendantRootPostNumber = requestedDescendantRootPostNumber ?? rootPostNumber;
	    this.#discussionRootPostNumber = rootPostNumber, this.#discussionDescendantRootPostNumber = descendantRootPostNumber, this.#discussionTargetPostNumber = targetPostNumber, this.#emit();
	    try {
	      if (this.#session.loadReplyBranches) {
	        const result = await this.#session.loadReplyBranches(
	          [descendantRootPostNumber],
	          { background: !1, maxPages: 32 }
	        );
	        if (epoch !== this.#discussionEpoch || this.scope.destroyed)
	          return this.snapshot();
	        this.#discussionBranchPartial ||= !result.complete;
	        for (const relation of result.contextualReplyRelations) {
	          const parentPostNumber = (0, import_identifiers.discoursePostNumber)(
	            relation.parentPostNumber
	          ), postNumber = (0, import_identifiers.discoursePostNumber)(relation.postNumber);
	          postNumber === parentPostNumber || !this.#session.postByNumber(parentPostNumber) || !this.#session.postByNumber(postNumber) || this.#discussionContextualParents.has(postNumber) || this.#discussionContextualParents.set(
	            postNumber,
	            parentPostNumber
	          );
	        }
	        for (const error of result.errors) this.#onError(error);
	      } else
	        this.#discussionUsesGlobalCoverage = !0, await this.#session.ensurePostStream({
	          background: !1,
	          maxAttempts: 2
	        });
	    } catch (error) {
	      epoch === this.#discussionEpoch && !this.scope.destroyed && (this.#discussionBranchPartial = !0, this.#onError(error));
	    }
	    return epoch !== this.#discussionEpoch || this.scope.destroyed ? this.snapshot() : (this.#discussionLoading = !1, this.#emit());
	  }
	  closeDiscussion() {
	    return this.#assertActive(), !this.#discussionRootPostNumber && !this.#discussionLoading ? this.snapshot() : (this.#discussionEpoch += 1, this.#discussionRootPostNumber = null, this.#discussionDescendantRootPostNumber = null, this.#discussionTargetPostNumber = null, this.#discussionLoading = !1, this.#discussionBranchPartial = !1, this.#discussionUsesGlobalCoverage = !1, this.#collapsedPostNumbers.clear(), this.#discussionContextualParents.clear(), this.#emit());
	  }
	  toggleDiscussionBranch(postNumberValue) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue);
	    return this.#collapsedPostNumbers.has(postNumber) ? this.#collapsedPostNumbers.delete(postNumber) : this.#collapsedPostNumbers.add(postNumber), this.#emit();
	  }
	  clearDiscussionTarget() {
	    return this.#assertActive(), this.#discussionTargetPostNumber === null ? this.snapshot() : (this.#discussionTargetPostNumber = null, this.#emit());
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #ensurePost(postNumber) {
	    const cached = this.#session.postByNumber(postNumber);
	    return cached || (await this.#session.loadTarget(postNumber, {
	      scope: "single",
	      advanceCursor: !1
	    }), this.#session.postByNumber(postNumber) ?? null);
	  }
	  async #branchRootOf(targetPostNumber, epoch) {
	    const resolution = await (0, import_reader_reply_ancestor_resolver.resolveReaderReplyAncestors)(
	      this.#session,
	      targetPostNumber,
	      {
	        stopBeforePostNumber: 1,
	        isActive: () => epoch === this.#discussionEpoch && !this.scope.destroyed
	      }
	    );
	    return !resolution.complete && epoch === this.#discussionEpoch && !this.scope.destroyed && (this.#discussionBranchPartial = !0), resolution.error !== void 0 && this.#onError(resolution.error), resolution.rootPostNumber;
	  }
	  #replyCount(post) {
	    const count = Number(post.reply_count ?? 0);
	    return Number.isSafeInteger(count) && count > 0 ? count : 0;
	  }
	  #discussionSnapshot() {
	    const rootPostNumber = this.#discussionRootPostNumber, descendantRootPostNumber = this.#discussionDescendantRootPostNumber;
	    if (rootPostNumber === null || descendantRootPostNumber === null)
	      return null;
	    const lineage = this.#discussionLineage(
	      rootPostNumber,
	      descendantRootPostNumber
	    ), lineageNext = /* @__PURE__ */ new Map();
	    for (let index = 0; index < lineage.length - 1; index += 1)
	      lineageNext.set(lineage[index], lineage[index + 1]);
	    const entries = [], pending = [{
	      postNumber: rootPostNumber,
	      parentPostNumber: null,
	      depth: 0
	    }], seen = /* @__PURE__ */ new Set();
	    let missingCanonicalPost = !1;
	    for (let index = 0; index < pending.length; index += 1) {
	      const entry = pending[index];
	      if (seen.has(entry.postNumber)) continue;
	      seen.add(entry.postNumber);
	      const post = this.#session.postByNumber(entry.postNumber);
	      post ? entries.push(Object.freeze({ ...entry, post })) : missingCanonicalPost = !0;
	      const nextLineagePostNumber = lineageNext.get(entry.postNumber);
	      for (const child of this.#discussionChildrenOf(entry.postNumber))
	        nextLineagePostNumber !== void 0 && child !== nextLineagePostNumber || pending.push(Object.freeze({
	          postNumber: (0, import_identifiers.discoursePostNumber)(child),
	          parentPostNumber: entry.postNumber,
	          depth: entry.depth + 1
	        }));
	    }
	    const postCoverage = this.#session.postStreamCoverage(), treeCoverage = this.#replies.coverage(), descendantEntry = entries.find((entry) => entry.postNumber === descendantRootPostNumber), branchHasMissingReplies = entries.some((entry) => entry.depth >= (descendantEntry?.depth ?? 0) && this.#replyCount(entry.post) > this.#discussionChildrenOf(entry.postNumber).length);
	    return Object.freeze({
	      rootPostNumber,
	      descendantRootPostNumber,
	      targetPostNumber: this.#discussionTargetPostNumber,
	      entries: Object.freeze(entries),
	      collapsedPostNumbers: frozenPostNumbers(
	        this.#collapsedPostNumbers
	      ),
	      loading: this.#discussionLoading,
	      partial: this.#discussionLoading || missingCanonicalPost || this.#discussionBranchPartial || branchHasMissingReplies || this.#discussionUsesGlobalCoverage && (!postCoverage.complete || !treeCoverage.complete)
	    });
	  }
	  #discussionChildrenOf(parentPostNumber) {
	    const children = /* @__PURE__ */ new Set();
	    for (const child of this.#replies.topology.childrenOf(parentPostNumber)) {
	      const postNumber = (0, import_identifiers.discoursePostNumber)(child), contextualParent = this.#discussionContextualParents.get(postNumber);
	      contextualParent !== void 0 && contextualParent !== parentPostNumber || children.add(postNumber);
	    }
	    for (const [postNumber, contextualParent] of this.#discussionContextualParents)
	      contextualParent === parentPostNumber && children.add(postNumber);
	    return Object.freeze([...children].sort((left, right) => left - right));
	  }
	  #discussionLineage(rootPostNumber, descendantRootPostNumber) {
	    const reversed = [], seen = /* @__PURE__ */ new Set();
	    let current = descendantRootPostNumber;
	    for (; !seen.has(current); ) {
	      if (seen.add(current), reversed.push(current), current === rootPostNumber)
	        return Object.freeze(reversed.reverse());
	      const parent = this.#replies.topology.parentOf(current);
	      if (parent == null) break;
	      current = (0, import_identifiers.discoursePostNumber)(parent);
	    }
	    return Object.freeze([rootPostNumber]);
	  }
	  #emit() {
	    if (this.scope.destroyed) return this.snapshot();
	    this.#revision += 1;
	    const snapshot = this.snapshot();
	    for (const error of this.changes.emit(snapshot)) this.#onError(error);
	    return snapshot;
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderTopicContextController 已销毁");
	  }
	}
}, "45e132944170204a8aebb5e07d6d70f747a0b720cadf4b3c37206bee7f165552");

/* Source: lite/src/topic/reader-topic-context-state.ts */
runtime.register("src/topic/reader-topic-context-state.js", function(module, exports, require) {
	var reader_topic_context_state_exports = {};
	__export(reader_topic_context_state_exports, {
	  READER_TOPIC_CONTEXT_STATE_KEY: () => READER_TOPIC_CONTEXT_STATE_KEY,
	  ReaderTopicContextStateRepository: () => ReaderTopicContextStateRepository,
	  readerTopicContextWebStorage: () => readerTopicContextWebStorage
	});
	module.exports = __toCommonJS(reader_topic_context_state_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_reader_account_scoped_storage = require("../state/reader-account-scoped-storage.js");
	const READER_TOPIC_CONTEXT_STATE_KEY = "linuxdo-enhanced-reader:reply-window:v1";
	function record(value) {
	  if (typeof value == "string")
	    try {
	      return record(JSON.parse(value));
	    } catch {
	      return null;
	    }
	  return value && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function finite(value, fallback = 0) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? numeric : fallback;
	}
	function geometry(value) {
	  const source = record(value);
	  if (!source) return null;
	  const width = finite(source.width), height = finite(source.height);
	  return !(width > 0) || !(height > 0) ? null : Object.freeze({
	    left: finite(source.left),
	    top: finite(source.top),
	    width,
	    height
	  });
	}
	function anchorPoint(value) {
	  const source = record(value);
	  if (!source) return null;
	  try {
	    return Object.freeze({
	      number: (0, import_identifiers.discoursePostNumber)(source.number),
	      scrollTop: Math.max(0, finite(source.scrollTop)),
	      scrollLeft: Math.max(0, finite(source.scrollLeft)),
	      offset: finite(source.offset, 12)
	    });
	  } catch {
	    return null;
	  }
	}
	function viewKey(host, topicId, rootPostNumber) {
	  return `${String(host).trim()}:${topicId}:${rootPostNumber}:0`;
	}
	function normalizedState(value, maxViews) {
	  const source = record(value), entries = [];
	  for (const [key, rawPoint] of Object.entries(record(source?.views) ?? {})) {
	    const point = anchorPoint(rawPoint);
	    point && entries.push(Object.freeze([
	      key,
	      Object.freeze({
	        ...point,
	        at: Math.max(0, finite(record(rawPoint)?.at))
	      })
	    ]));
	  }
	  return entries.sort((left, right) => left[1].at - right[1].at), Object.freeze({
	    fullPageGeometry: geometry(source?.fullPageGeometry),
	    views: Object.freeze(Object.fromEntries(entries.slice(-maxViews)))
	  });
	}
	class ReaderTopicContextStateRepository {
	  #storage;
	  #key;
	  #accountStorage;
	  #maxViews;
	  #onError;
	  #state;
	  #write = Promise.resolve();
	  #loadPromise = null;
	  constructor(options) {
	    if (this.#storage = options.storage, this.#accountStorage = options.key === void 0 && options.authScope !== void 0 ? (0, import_reader_account_scoped_storage.readerAccountScopedStorageIdentity)(
	      READER_TOPIC_CONTEXT_STATE_KEY,
	      options.authScope
	    ) : null, this.#key = String(options.key ?? this.#accountStorage?.key ?? READER_TOPIC_CONTEXT_STATE_KEY).trim(), !this.#key) throw new Error("完整讨论状态 storage key 不能为空");
	    const maxViews = Math.floor(Number(options.maxViews ?? 128));
	    if (!Number.isSafeInteger(maxViews) || maxViews < 1)
	      throw new RangeError("完整讨论持久锚点上限必须是正整数");
	    this.#maxViews = maxViews, this.#onError = options.onError ?? (() => {
	    }), this.#state = normalizedState(null, maxViews);
	  }
	  get snapshot() {
	    return this.#state;
	  }
	  load() {
	    return this.#loadPromise ? this.#loadPromise : (this.#loadPromise = (async () => {
	      try {
	        const loaded = normalizedState(
	          this.#accountStorage ? await (0, import_reader_account_scoped_storage.readReaderAccountScopedValue)(
	            this.#storage,
	            this.#accountStorage
	          ) : await this.#storage.getValue(this.#key),
	          this.#maxViews
	        ), current = this.#state, hasLocalState = current.fullPageGeometry !== null || Object.keys(current.views).length > 0;
	        this.#state = normalizedState({
	          fullPageGeometry: current.fullPageGeometry ?? loaded.fullPageGeometry,
	          views: {
	            ...loaded.views,
	            ...current.views
	          }
	        }, this.#maxViews), hasLocalState && this.#persist();
	      } catch (error) {
	        this.#onError(error);
	      }
	      return this.#state;
	    })(), this.#loadPromise);
	  }
	  point(host, topicIdValue, rootPostNumberValue) {
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), rootPostNumber = (0, import_identifiers.discoursePostNumber)(rootPostNumberValue), value = this.#state.views[viewKey(host, topicId, rootPostNumber)];
	    return value ? Object.freeze({
	      number: value.number,
	      scrollTop: value.scrollTop,
	      scrollLeft: value.scrollLeft,
	      offset: value.offset
	    }) : null;
	  }
	  rememberGeometry(value) {
	    const nextGeometry = geometry(value);
	    nextGeometry && (this.#state = Object.freeze({
	      ...this.#state,
	      fullPageGeometry: nextGeometry
	    }), this.#persist());
	  }
	  rememberPoint(host, topicIdValue, rootPostNumberValue, point, now = Date.now()) {
	    if (!point) return;
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), rootPostNumber = (0, import_identifiers.discoursePostNumber)(rootPostNumberValue), key = viewKey(host, topicId, rootPostNumber), entries = Object.entries(this.#state.views).filter(
	      ([entryKey]) => entryKey !== key
	    );
	    entries.push([
	      key,
	      Object.freeze({
	        ...point,
	        at: Math.max(0, finite(now))
	      })
	    ]);
	    const views = Object.fromEntries(entries);
	    this.#state = normalizedState({
	      fullPageGeometry: this.#state.fullPageGeometry,
	      views
	    }, this.#maxViews), this.#persist();
	  }
	  replaceExternal(value) {
	    return this.#state = normalizedState(value, this.#maxViews), this.#persist(), this.#state;
	  }
	  async flush() {
	    await this.#write;
	  }
	  #persist() {
	    const state = this.#state;
	    this.#write = this.#write.catch(() => {
	    }).then(async () => {
	      try {
	        await this.#storage.setValue(
	          this.#key,
	          state
	        );
	      } catch (error) {
	        this.#onError(error);
	      }
	    });
	  }
	}
	function readerTopicContextWebStorage(storage) {
	  return Object.freeze({
	    getValue: (key) => storage.getItem(key),
	    setValue: (key, value) => {
	      storage.setItem(key, JSON.stringify(value));
	    }
	  });
	}
}, "9f024429af2d4fe1570b1e820d3a65516eb5108af753cae8ad821c881298be94");

/* Source: lite/src/topic/reader-topic-context-surface.ts */
runtime.register("src/topic/reader-topic-context-surface.js", function(module, exports, require) {
	var reader_topic_context_surface_exports = {};
	__export(reader_topic_context_surface_exports, {
	  ReaderTopicContextFeature: () => ReaderTopicContextFeature,
	  ReaderTopicContextSurface: () => ReaderTopicContextSurface
	});
	module.exports = __toCommonJS(reader_topic_context_surface_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_event_target = require("../dom/event-target.js"), import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_html_element = require("../dom/html-element.js"), import_reply_tree_dom_owner = require("../dom/reply-tree-dom-owner.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_icon = require("../components/reader-icon.js"), import_branch_overlay = require("../layout/branch-overlay.js"), import_reader_workspace = require("../shell/reader-workspace.js"), import_reader_escape_surface = require("../shell/reader-escape-surface.js"), import_reader_post_view_projector = require("./reader-post-view-projector.js"), import_reader_topic_context_state = require("./reader-topic-context-state.js");
	function button(document, className, label, text = "") {
	  const value = (0, import_html_element.htmlElement)(document, "button", className);
	  return value.type = "button", value.setAttribute("aria-label", label), value.textContent = text, value;
	}
	const HIDDEN_REPLY_MATERIALIZE_BATCH_SIZE = 100, QUOTE_HINT_POINTER_GAP_PX = 4, QUOTE_HINT_HIDE_GRACE_MS = 480;
	function postCooked(post) {
	  return String(
	    post.cooked ?? ""
	  );
	}
	function postReplyCount(post) {
	  const value = Number(
	    post.reply_count ?? 0
	  );
	  return Number.isSafeInteger(value) && value > 0 ? value : 0;
	}
	function comparableQuoteText(value) {
	  return value.replace(/[ \t\r\n]/g, "");
	}
	function quoteExcerptText(document, html) {
	  const container = document.createElement("div");
	  return container.innerHTML = html, String(container.textContent ?? "").trim();
	}
	function usableQuoteHintRect(rect) {
	  return [rect.left, rect.top, rect.right, rect.bottom, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
	}
	function quoteHintElementRects(element2) {
	  const clientRects = typeof element2.getClientRects == "function" ? Array.from(element2.getClientRects()).filter(usableQuoteHintRect) : [];
	  if (clientRects.length) return clientRects;
	  const bounds = element2.getBoundingClientRect();
	  return usableQuoteHintRect(bounds) ? Object.freeze([bounds]) : Object.freeze([]);
	}
	function markQuoteTextMatch(document, root, text) {
	  const comparable = comparableQuoteText(text);
	  if (!comparable) return Object.freeze([]);
	  const characters = [], positions = [], showText = document.defaultView?.NodeFilter?.SHOW_TEXT ?? 4, walker = document.createTreeWalker(root, showText);
	  for (let node = walker.nextNode(); node; node = walker.nextNode()) {
	    const textNode = node, parent = textNode.parentElement;
	    if (!parent || parent.closest(
	      "button,script,style,noscript,.ldp-quote-highlight-close"
	    ))
	      continue;
	    const value = textNode.nodeValue ?? "";
	    for (let offset = 0; offset < value.length; offset += 1) {
	      const character = value[offset];
	      /[ \t\r\n]/.test(character) || (characters.push(character), positions.push(Object.freeze({ node: textNode, offset })));
	    }
	  }
	  const matchStart = characters.join("").indexOf(comparable);
	  if (matchStart < 0) return Object.freeze([]);
	  const groups = [];
	  for (let index = matchStart; index < matchStart + comparable.length; index += 1) {
	    const position = positions[index];
	    if (!position) continue;
	    const previous = groups.at(-1);
	    previous?.node === position.node ? previous.end = position.offset + 1 : groups.push({
	      node: position.node,
	      start: position.offset,
	      end: position.offset + 1
	    });
	  }
	  const marks = [];
	  for (let index = groups.length - 1; index >= 0; index -= 1) {
	    const group = groups[index], parent = group.node.parentNode;
	    if (!parent) continue;
	    const value = group.node.nodeValue ?? "", fragment = document.createDocumentFragment();
	    group.start > 0 && fragment.append(document.createTextNode(value.slice(0, group.start)));
	    const mark = document.createElement("mark");
	    mark.className = "ldp-quote-match", mark.textContent = value.slice(group.start, group.end), fragment.append(mark), group.end < value.length && fragment.append(document.createTextNode(value.slice(group.end))), parent.replaceChild(fragment, group.node), marks.unshift(mark);
	  }
	  return Object.freeze(marks);
	}
	class ReaderTopicContextFeature {
	  activationScope = "branch";
	  scope;
	  #document;
	  #controller;
	  #replies;
	  #presentation;
	  #scrollRoot;
	  #navigate;
	  #target;
	  #avatarSource;
	  #renderIcon;
	  #onQuoteBodyChanged;
	  #onRevealNextReplyLevel;
	  #revealQuoteTarget;
	  #navigationRetryDelay;
	  #quoteHintHost;
	  #notify;
	  #onError;
	  #rootCleanups = /* @__PURE__ */ new WeakMap();
	  #viewByRoot = /* @__PURE__ */ new WeakMap();
	  #hiddenReplyMarkers = /* @__PURE__ */ new Map();
	  #hiddenReplyPostNumbers = /* @__PURE__ */ new WeakMap();
	  #rootsByPostNumber = /* @__PURE__ */ new Map();
	  #collapsedRootReplies = /* @__PURE__ */ new Set();
	  #quoteJumpExcerptByElement = /* @__PURE__ */ new WeakMap();
	  #quotePostLoads = /* @__PURE__ */ new Map();
	  #expandedQuoteKeys = /* @__PURE__ */ new Set();
	  #quoteSourcePort = null;
	  #quoteHighlight = null;
	  #quoteHighlightMarks = Object.freeze([]);
	  #quoteHighlightHint = null;
	  #quoteReturnEntries = /* @__PURE__ */ new Set();
	  #quoteHintHideTimer = null;
	  #quotePositionEpoch = 0;
	  #quoteJumpEpoch = 0;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#document = options.document, this.#controller = options.controller, this.#replies = options.replies, this.#presentation = options.presentation, this.#scrollRoot = options.scrollRoot, this.#navigate = options.navigate, this.#target = options.target, this.#avatarSource = options.avatarSource, this.#renderIcon = options.renderIcon, this.#onQuoteBodyChanged = options.onQuoteBodyChanged, this.#onRevealNextReplyLevel = options.onRevealNextReplyLevel, this.#revealQuoteTarget = options.revealQuoteTarget, this.#navigationRetryDelay = options.navigationRetryDelay ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))), this.#quoteHintHost = options.quoteHintHost ?? options.document.body ?? options.document.documentElement, this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    });
	    for (const type of [
	      "ldp-reader-window-change",
	      "ldp-reader-workspace-change"
	    ])
	      this.scope.listen(this.#quoteHintHost, type, () => {
	        this.#quoteHighlightHint?.classList.remove(
	          "ldp-quote-hint-visible"
	        );
	      });
	    (options.presentationChanges ?? this.#replies.changes).subscribe(() => {
	      for (const roots of this.#rootsByPostNumber.values())
	        for (const root of roots) this.#syncMountedTree(root);
	    }, this.scope), this.scope.add(() => {
	      this.#quoteJumpEpoch += 1, this.#quotePositionEpoch += 1, this.#scrollRoot.classList.remove("ldp-quote-positioning");
	      for (const roots of this.#rootsByPostNumber.values())
	        for (const root of roots) this.#rootCleanups.get(root)?.();
	      this.#rootsByPostNumber.clear(), this.#collapsedRootReplies.clear(), this.#quotePostLoads.clear(), this.#expandedQuoteKeys.clear(), this.#clearQuoteHighlight();
	    });
	  }
	  afterRender(post, view) {
	    this.#viewByRoot.set(view.slots.root, view), this.#prepareQuotes(post, view), this.#syncRelationshipControls(
	      view.slots.root,
	      (0, import_identifiers.discoursePostReference)(post).postNumber,
	      post
	    );
	    const highlight = this.#quoteHighlight;
	    highlight?.postNumber === view.postNumber && highlight.source && this.#mountQuoteReturnEntry(view.slots.root, highlight.source), highlight?.postNumber === view.postNumber && this.#quoteHighlightMarks.every((mark) => !mark.parentNode) && this.#applyQuoteHighlight(
	      view.slots.root,
	      highlight.text,
	      highlight.active,
	      highlight.source
	    );
	  }
	  attachRoot(root, postNumberValue) {
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue);
	    if (this.#rootCleanups.has(root)) return;
	    const roots = this.#rootsByPostNumber.get(postNumber) ?? /* @__PURE__ */ new Set();
	    roots.add(root), this.#rootsByPostNumber.set(postNumber, roots);
	    const onClick = (event) => {
	      this.#handleClick(event, root, postNumber).catch((error) => {
	        !this.scope.destroyed && this.#rootCleanups.has(root) && this.#onError(error);
	      });
	    };
	    root.addEventListener("click", onClick);
	    const cleanup = () => {
	      for (const postRoot of [
	        root,
	        ...root.querySelectorAll(".ldp-post")
	      ])
	        this.#removeHiddenReplyMarker(postRoot), postRoot.classList.remove("ldp-has-hidden-child-branches"), postRoot.querySelectorAll(
	          ":scope > .ldp-reply-tree > .ldp-reply-controls > [data-reader-context-hidden-branch-controls]"
	        ).forEach((control) => control.remove());
	      root.removeEventListener("click", onClick);
	      for (const entry of root.querySelectorAll(
	        "[data-reader-context-quote-return]"
	      ))
	        this.#quoteReturnEntries.delete(entry), entry.remove();
	      this.#rootCleanups.delete(root);
	      const current = this.#rootsByPostNumber.get(postNumber);
	      current?.delete(root), current?.size || this.#rootsByPostNumber.delete(postNumber);
	    };
	    this.#rootCleanups.set(root, cleanup), this.#syncMountedTree(root);
	    const highlight = this.#quoteHighlight;
	    highlight?.postNumber === postNumber && highlight.source && this.#mountQuoteReturnEntry(root, highlight.source);
	  }
	  detachRoot(root) {
	    this.#rootCleanups.get(root)?.();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  syncProjection() {
	    for (const roots of this.#rootsByPostNumber.values())
	      for (const root of roots) this.#syncMountedTree(root);
	  }
	  /** 收起当前视口内一个作为普通楼层显示的叶子回复;优先鼠标所在楼层。 */
	  collapseExpandedDefaultPost() {
	    const viewport = this.#scrollRoot.getBoundingClientRect(), candidates = [...this.#rootsByPostNumber.entries()].flatMap(([postNumber, roots]) => [...roots].map((root) => ({
	      postNumber,
	      root,
	      rect: root.getBoundingClientRect()
	    }))).filter(
	      ({ root, rect }) => root.isConnected && !root.closest(".ldp-descendant-replies-layer") && root.classList.contains("ldp-reply-collapsible") && !root.classList.contains("ldp-nested-collapsed") && !root.classList.contains("ldp-nested-preview") && rect.bottom > viewport.top && rect.top < viewport.bottom && rect.right > viewport.left && rect.left < viewport.right
	    ).sort((left, right) => left.rect.top - right.rect.top), candidate = candidates.find(({ root }) => {
	      try {
	        return root.matches(":hover");
	      } catch {
	        return !1;
	      }
	    }) ?? candidates[0];
	    return candidate ? (this.#setRootReplyCollapsed(
	      candidate.root,
	      candidate.postNumber,
	      !0
	    ), candidate.postNumber) : null;
	  }
	  connectQuoteSource(port) {
	    if (this.scope.destroyed)
	      throw new Error("线程上下文 feature 已销毁");
	    if (this.#quoteSourcePort && this.#quoteSourcePort !== port)
	      throw new Error("引用来源端口只能连接一次");
	    this.#quoteSourcePort = port;
	    let active = !0;
	    const cleanup = () => {
	      active && (active = !1, this.#quoteSourcePort === port && (this.#quoteSourcePort = null));
	    };
	    return this.scope.add(cleanup), cleanup;
	  }
	  captureQuoteHighlightState() {
	    return this.#quoteHighlight;
	  }
	  applyRevealedQuoteHighlight(state, postRoot = null) {
	    const targetRoot = this.#resolveQuoteTargetRoot(state.postNumber, postRoot);
	    if (!targetRoot)
	      return this.#clearQuoteHighlight(), this.#quoteHighlight = state, !1;
	    const matched = this.#applyQuoteHighlight(
	      targetRoot,
	      state.text,
	      state.active,
	      state.source,
	      !0
	    );
	    return matched || this.#revealQuoteTarget?.(targetRoot, "floor"), matched;
	  }
	  #resolveQuoteTargetRoot(postNumber, preferred) {
	    const expected = String(postNumber);
	    return preferred?.dataset.postNumber === expected && preferred.isConnected ? preferred : [...this.#rootsByPostNumber.get(postNumber) ?? []].find((root) => root.isConnected && root.dataset.postNumber === expected) ?? null;
	  }
	  async restoreQuoteHighlightState(state) {
	    if (!state)
	      return this.#clearQuoteHighlight(), !0;
	    const navigation = this.#navigate();
	    if (!navigation) return !1;
	    const result = await navigation.navigate({
	      postNumber: state.postNumber,
	      source: "quote",
	      alignment: "nearest",
	      highlight: !1
	    });
	    return this.scope.destroyed || result.status !== "revealed" || !result.element ? !1 : this.applyRevealedQuoteHighlight(state, result.element);
	  }
	  #icon(name) {
	    return (0, import_reader_icon.renderReaderIcon)(this.#document, name, this.#renderIcon);
	  }
	  #prepareQuotes(post, view) {
	    const sourcePostNumber = (0, import_identifiers.discoursePostReference)(post).postNumber;
	    for (const quote of view.slots.content.querySelectorAll(
	      "aside.quote"
	    )) {
	      const title = quote.querySelector(":scope > .title"), body = quote.querySelector(":scope > blockquote");
	      if (!title || !body) continue;
	      quote.classList.add("ldp-post-quote"), title.classList.add("ldp-quote-title");
	      const targetPostNumber = Number(quote.dataset.post ?? 0), targetTopicId = Number(
	        quote.dataset.topic ?? this.#controller.topicId
	      );
	      if (!Number.isSafeInteger(targetPostNumber) || targetPostNumber < 1 || !Number.isSafeInteger(targetTopicId) || targetTopicId < 1)
	        continue;
	      this.#quoteJumpExcerptByElement.has(quote) || this.#quoteJumpExcerptByElement.set(quote, body.innerHTML);
	      let controls = title.querySelector(
	        ":scope > .quote-controls"
	      );
	      controls || (controls = (0, import_html_element.htmlElement)(this.#document, "span", "quote-controls"), title.append(controls)), controls.classList.add("ldp-quote-controls"), controls.querySelectorAll("[data-reader-context-quote]").forEach((control) => control.remove());
	      const key = `${sourcePostNumber}:${targetTopicId}:${targetPostNumber}`, expanded = this.#expandedQuoteKeys.has(key);
	      quote.classList.toggle("ldp-quote-expanded", expanded), quote.dataset.ldpQuoteExpanded = expanded ? "1" : "0", expanded || this.#restoreQuoteExcerpt(quote, body);
	      const toggle = button(
	        this.#document,
	        "ldp-quote-toggle",
	        expanded ? "收起引用" : "展开完整引用"
	      );
	      toggle.dataset.readerContextQuote = "toggle", toggle.dataset.quoteKey = key, toggle.dataset.targetPostNumber = String(targetPostNumber), toggle.dataset.targetTopicId = String(targetTopicId), toggle.setAttribute("aria-expanded", String(expanded)), toggle.append(this.#icon(
	        expanded ? "chevron-up" : "chevron-down"
	      )), controls.append(toggle);
	      const jump = button(
	        this.#document,
	        "ldp-quote-jump",
	        `跳到被引用楼层 #${targetPostNumber}`
	      );
	      if (jump.dataset.readerContextQuote = "jump", jump.dataset.targetPostNumber = String(targetPostNumber), jump.dataset.targetTopicId = String(targetTopicId), jump.append(this.#icon("arrow-up")), controls.append(jump), !expanded) continue;
	      const fullPost = this.#controller.quotedPost(
	        targetTopicId,
	        targetPostNumber
	      );
	      if (fullPost) {
	        this.#applyQuotePost(quote, body, fullPost);
	        continue;
	      }
	      this.#hydrateExpandedQuoteBody(
	        view,
	        quote,
	        body,
	        key,
	        targetTopicId,
	        targetPostNumber
	      ).catch((error) => {
	        this.scope.destroyed || this.#onError(error);
	      });
	    }
	  }
	  #restoreQuoteExcerpt(quote, body) {
	    const excerpt = this.#quoteJumpExcerptByElement.get(quote);
	    return delete quote.dataset.ldpQuoteHydrated, excerpt === void 0 || body.innerHTML === excerpt ? !1 : (body.innerHTML = excerpt, !0);
	  }
	  #applyQuotePost(quote, body, fullPost) {
	    const cooked = postCooked(fullPost);
	    return quote.dataset.ldpQuoteHydrated = "1", body.innerHTML === cooked ? !1 : (body.innerHTML = cooked, !0);
	  }
	  async #hydrateExpandedQuoteBody(view, quote, body, key, targetTopicId, targetPostNumber) {
	    const fullPost = await this.#loadQuotePost(
	      targetTopicId,
	      targetPostNumber
	    );
	    !fullPost || !this.#expandedQuoteKeys.has(key) || this.scope.destroyed || this.#viewByRoot.get(view.slots.root) !== view || !view.slots.content.contains(quote) || quote.querySelector(":scope > blockquote") !== body || this.#applyQuotePost(quote, body, fullPost) && this.#notifyQuoteBodyChanged(view.slots.root, "expanded");
	  }
	  async #loadQuotePost(targetTopicId, targetPostNumber) {
	    const cached = this.#controller.quotedPost(
	      targetTopicId,
	      targetPostNumber
	    );
	    if (cached) return cached;
	    const key = `${targetTopicId}:${targetPostNumber}`, pending = this.#quotePostLoads.get(key);
	    if (pending) return pending;
	    const request = this.#requestQuotePost(
	      key,
	      targetTopicId,
	      targetPostNumber
	    );
	    return this.#quotePostLoads.set(key, request), request;
	  }
	  async #requestQuotePost(key, targetTopicId, targetPostNumber) {
	    try {
	      return await this.#controller.loadQuotedPost(
	        targetTopicId,
	        targetPostNumber
	      );
	    } finally {
	      this.#quotePostLoads.delete(key);
	    }
	  }
	  #syncRelationshipControls(root, postNumber, post = null) {
	    const header = root.querySelector(
	      ":scope > .ldp-post-head"
	    ), replyControls = root.querySelector(
	      ":scope > .ldp-reply-tree > .ldp-reply-controls"
	    );
	    header?.querySelectorAll(":scope > [data-reader-context-parent]").forEach((control) => control.remove()), header?.querySelectorAll(":scope > .ldp-jump-parent").forEach((control) => control.remove()), replyControls?.querySelectorAll(":scope > [data-reader-context-discussion]").forEach((control) => control.remove()), replyControls?.querySelectorAll(
	      ":scope > [data-reader-context-hidden-branch-controls]"
	    ).forEach((control) => control.remove()), root.classList.remove("ldp-has-hidden-child-branches");
	    const parent = this.#replies.topology.parentOf(postNumber), onlyOpPost = this.#isOnlyOpPost(postNumber), currentFloor = header?.querySelector(
	      ":scope > :is(.ldp-body-floor,[data-reader-context-self])"
	    );
	    if (header && parent !== void 0 && parent !== null)
	      if (currentFloor?.dataset.readerContextSelf)
	        currentFloor.classList.add("ldp-current-floor"), currentFloor.textContent = `#${postNumber}`, currentFloor.dataset.targetPostNumber = String(postNumber), currentFloor.setAttribute("aria-label", `跳到楼层 #${postNumber}`);
	      else {
	        const selfButton = button(
	          this.#document,
	          "ldp-floor ldp-jump-self ldp-current-floor",
	          `跳到楼层 #${postNumber}`,
	          `#${postNumber}`
	        );
	        selfButton.dataset.readerContextSelf = "1", selfButton.dataset.targetPostNumber = String(postNumber), currentFloor ? currentFloor.replaceWith(selfButton) : header.insertBefore(
	          selfButton,
	          header.querySelector(":scope > .ldp-post-read-state")
	        );
	      }
	    else if (header && currentFloor?.dataset.readerContextSelf) {
	      const floor = (0, import_html_element.htmlElement)(this.#document, "span", "ldp-floor ldp-body-floor");
	      floor.textContent = `#${postNumber}`, currentFloor.replaceWith(floor);
	    }
	    const insideDiscussion = !!root.closest(".ldp-descendant-replies-layer"), hiddenDirectChildren = insideDiscussion ? Object.freeze([]) : this.#presentation?.hiddenDirectChildrenOf(postNumber) ?? Object.freeze([]);
	    if (replyControls && hiddenDirectChildren.length > 0) {
	      root.classList.add("ldp-has-hidden-child-branches");
	      const controls = (0, import_html_element.htmlElement)(
	        this.#document,
	        "div",
	        "ldp-hidden-branch-controls"
	      );
	      controls.dataset.readerContextHiddenBranchControls = "1";
	      const revealButton = button(
	        this.#document,
	        "ldp-collapse-replies ldp-reply-rail-control ldp-hidden-branch-reveal",
	        `展开楼层 #${postNumber} 的下一层回复`
	      );
	      revealButton.append(this.#icon("plus")), revealButton.dataset.readerContextRevealBranch = "1", revealButton.setAttribute("aria-expanded", "false");
	      const branchButton = button(
	        this.#document,
	        "ldp-btn ldp-sub-page-btn ldp-hidden-branch-discussion",
	        `查看楼层 #${postNumber} 以下的完整分支`
	      );
	      branchButton.dataset.readerContextBranchDiscussion = "1", branchButton.dataset.targetPostNumber = String(
	        hiddenDirectChildren[0]
	      ), branchButton.append(this.#icon("layers"));
	      const label = (0, import_html_element.htmlElement)(this.#document, "span", "");
	      label.textContent = "查看完整分支", branchButton.append(label), controls.append(revealButton, branchButton), replyControls.append(controls);
	    }
	    const discussionOwner = this.#discussionBranchOwner(postNumber), onlyOpDiscussion = onlyOpPost && parent !== void 0 && parent !== null && !insideDiscussion, hasParkedDiscussion = postNumber > 1 && !insideDiscussion && discussionOwner === postNumber && this.#branchHasParkedDiscussion(postNumber, post);
	    if (replyControls && (onlyOpDiscussion || hasParkedDiscussion)) {
	      const discussionButton = button(
	        this.#document,
	        "ldp-btn ldp-sub-page-btn ldp-descendant-replies-open",
	        `查看楼层 #${postNumber} 的完整讨论`
	      );
	      discussionButton.dataset.readerContextDiscussion = "1", discussionButton.append(this.#icon("layers"));
	      const label = (0, import_html_element.htmlElement)(this.#document, "span", "");
	      label.textContent = "查看完整讨论", discussionButton.append(label), replyControls.append(discussionButton);
	    }
	  }
	  #syncRootReplyCollapse(root, postNumber, post) {
	    const canonicalParent = this.#replies.topology.parentOf(postNumber), projectedParent = this.#presentation?.parentOf(postNumber), canonicalReply = post !== null && canonicalParent !== void 0 && canonicalParent !== null, canonicalLeafReply = canonicalReply && postReplyCount(post) === 0 && this.#replies.topology.childrenOf(postNumber).length === 0, nestedPreview = root.classList.contains("ldp-nested-preview"), streamReply = canonicalReply && !this.#isOnlyOpPost(postNumber) && !root.closest(".ldp-descendant-replies-layer") && (projectedParent === null || nestedPreview), collapseControlVisible = streamReply && projectedParent === null && !nestedPreview, previousToggle = root.querySelector(
	      ":scope > [data-reader-context-collapse-reply]"
	    ), previousHint = root.querySelector(
	      ":scope > .ldp-nested-esc-hint"
	    );
	    if (!streamReply) {
	      canonicalLeafReply || this.#collapsedRootReplies.delete(postNumber), root.classList.remove(
	        "ldp-reply",
	        "ldp-reply-collapsible",
	        "ldp-nested-collapsed"
	      ), delete root.dataset.replyToPostNumber, previousToggle?.remove(), previousHint?.remove();
	      return;
	    }
	    if (root.classList.add("ldp-reply", "ldp-reply-collapsible"), root.dataset.replyToPostNumber = String(canonicalParent), !collapseControlVisible) {
	      root.classList.remove("ldp-nested-collapsed"), previousToggle?.remove(), previousHint?.remove();
	      return;
	    }
	    let toggle = previousToggle;
	    toggle || (toggle = button(
	      this.#document,
	      "ldp-btn ldp-nested-toggle",
	      ""
	    ), toggle.dataset.readerContextCollapseReply = "1", root.insertBefore(toggle, root.firstChild)), this.#setRootReplyCollapsed(
	      root,
	      postNumber,
	      this.#collapsedRootReplies.has(postNumber)
	    );
	  }
	  #setRootReplyCollapsed(root, postNumber, collapsed) {
	    collapsed ? this.#collapsedRootReplies.add(postNumber) : this.#collapsedRootReplies.delete(postNumber), root.classList.toggle("ldp-nested-collapsed", collapsed);
	    const toggle = root.querySelector(
	      ":scope > [data-reader-context-collapse-reply]"
	    );
	    toggle && (toggle.setAttribute("aria-expanded", String(!collapsed)), toggle.setAttribute(
	      "aria-label",
	      `${collapsed ? "展开" : "收起"}楼层 #${postNumber}`
	    ), toggle.replaceChildren(this.#icon(
	      collapsed ? "chevron-down" : "chevron-up"
	    )));
	    let hint = root.querySelector(
	      ":scope > .ldp-nested-esc-hint"
	    );
	    if (collapsed) {
	      hint?.remove();
	      return;
	    }
	    !hint && toggle && (hint = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-nested-esc-hint"
	    ), toggle.insertAdjacentElement("afterend", hint)), hint && (hint.textContent = "Esc 收起");
	  }
	  #isOnlyOpPost(postNumber) {
	    return this.#presentation?.postFilterKey?.startsWith("only-op:") === !0 && this.#presentation.postFilterMatches(postNumber);
	  }
	  /**
	   * 一条连续可见树只允许一个完整讨论入口。
	   *
	   * 普通树由 projection root 持有;楼主 #1 下的每条直属回复分支分别由该直属回复
	   * 持有,避免把所有评论误合并为楼主的一条讨论。入口放在 owner 的 replyControls,
	   * 该槽位在内联 replyList 之后,因此视觉上天然位于整条可见分支末尾。
	   */
	  #discussionBranchOwner(postNumber) {
	    const presentation = this.#presentation;
	    if (!presentation) return postNumber;
	    const rootPostNumber = presentation.rootOf(postNumber);
	    if (rootPostNumber === void 0 || rootPostNumber !== 1)
	      return (0, import_identifiers.discoursePostNumber)(rootPostNumber ?? postNumber);
	    let owner = postNumber, parent = presentation.parentOf(owner);
	    for (; parent != null && parent !== 1; )
	      owner = (0, import_identifiers.discoursePostNumber)(parent), parent = presentation.parentOf(owner);
	    return (0, import_identifiers.discoursePostNumber)(owner);
	  }
	  #branchHasParkedDiscussion(rootPostNumber, rootPost) {
	    const presentation = this.#presentation, pending = [{ postNumber: rootPostNumber, post: rootPost }], visited = /* @__PURE__ */ new Set();
	    for (; pending.length; ) {
	      const current = pending.pop();
	      if (visited.has(current.postNumber)) continue;
	      visited.add(current.postNumber);
	      const knownChildren = this.#replies.topology.childrenOf(
	        current.postNumber
	      ), visibleChildren = presentation?.childrenOf(current.postNumber) ?? knownChildren, hiddenChildren = presentation?.hiddenDirectChildrenOf(
	        current.postNumber
	      ).length ?? 0, currentPost = current.post ?? this.#controller.postByNumber(current.postNumber), unresolvedChildren = Math.max(
	        0,
	        (currentPost ? postReplyCount(currentPost) : 0) - knownChildren.length
	      );
	      if (hiddenChildren > 0 || unresolvedChildren > 0 && visibleChildren.length === 0) return !0;
	      for (const childPostNumber of visibleChildren) {
	        const normalizedChild = (0, import_identifiers.discoursePostNumber)(childPostNumber);
	        pending.push({
	          postNumber: normalizedChild,
	          post: this.#controller.postByNumber(normalizedChild) ?? null
	        });
	      }
	    }
	    return !1;
	  }
	  #syncMountedTree(root) {
	    const postRoots = [
	      root,
	      ...root.querySelectorAll(".ldp-post")
	    ];
	    for (const postRoot of postRoots) {
	      const postNumber = (0, import_identifiers.discoursePostNumber)(
	        postRoot.dataset.postNumber
	      ), post = this.#controller.postByNumber(postNumber) ?? null;
	      this.#syncRelationshipControls(
	        postRoot,
	        postNumber,
	        post
	      ), this.#syncRootReplyCollapse(
	        postRoot,
	        postNumber,
	        post
	      ), !postRoot.closest(".ldp-descendant-replies-layer") && this.#syncHiddenReplyMarker(postRoot, postNumber);
	    }
	  }
	  #syncHiddenReplyMarker(root, postNumber) {
	    if (this.#presentation && this.#presentation.rootOf(postNumber) !== postNumber) {
	      this.#removeHiddenReplyMarker(root);
	      return;
	    }
	    const previous = this.#hiddenReplyMarkers.get(root) ?? null, hiddenPostNumbers = this.#presentation?.hiddenFloorRunAfter(postNumber) ?? Object.freeze([]);
	    if (hiddenPostNumbers.length === 0) {
	      this.#removeHiddenReplyMarker(root);
	      return;
	    }
	    if (previous && this.#hiddenReplyPostNumbers.get(previous) === hiddenPostNumbers) {
	      root.classList.add("ldp-before-hidden-reply-marker"), previous.previousElementSibling !== root && root.after(previous);
	      return;
	    }
	    const expanded = previous?.querySelector(
	      "[data-reader-context-hidden-list]"
	    )?.hidden === !1;
	    this.#removeHiddenReplyMarker(root);
	    const marker = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-hidden-reply-marker"
	    );
	    marker.dataset.readerContextHiddenReplies = "1", marker.dataset.readerContextHiddenPostNumbers = [
	      hiddenPostNumbers.length,
	      hiddenPostNumbers[0],
	      hiddenPostNumbers.at(-1)
	    ].join(":"), this.#hiddenReplyPostNumbers.set(marker, hiddenPostNumbers);
	    const toggle = button(
	      this.#document,
	      "ldp-btn ldp-hidden-reply-toggle",
	      `${expanded ? "收起" : "查看"} ${hiddenPostNumbers.length} 个隐藏回复`
	    );
	    toggle.dataset.readerContextHiddenToggle = "1", toggle.setAttribute("aria-expanded", String(expanded));
	    const list = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-hidden-reply-list"
	    );
	    list.dataset.readerContextHiddenList = "1", list.dataset.readerContextHiddenRendered = "0", list.setAttribute("role", "list"), list.hidden = !expanded, expanded && this.#appendHiddenReplyBatch(marker, list), marker.append(toggle, list), this.#hiddenReplyMarkers.set(root, marker), root.classList.add("ldp-before-hidden-reply-marker"), marker.addEventListener("click", (event) => {
	      this.#handleClick(event, root, postNumber).catch((error) => {
	        !this.scope.destroyed && marker.isConnected && this.#onError(error);
	      });
	    }), root.after(marker);
	  }
	  #appendHiddenReplyBatch(marker, list) {
	    list.querySelector("[data-reader-context-hidden-more]")?.remove();
	    const postNumbers = this.#hiddenReplyPostNumbers.get(marker) ?? [], rendered = Math.min(
	      postNumbers.length,
	      Math.max(
	        0,
	        Number(list.dataset.readerContextHiddenRendered) || 0
	      )
	    ), end = Math.min(
	      postNumbers.length,
	      rendered + HIDDEN_REPLY_MATERIALIZE_BATCH_SIZE
	    );
	    for (let index = rendered; index < end; index += 1) {
	      const hiddenPostNumber = postNumbers[index], identity = this.#controller.postByNumber(hiddenPostNumber), username = String(identity?.username ?? "").trim(), displayName = String(identity?.name ?? "").trim() || username || `#${hiddenPostNumber}`, avatar = button(
	        this.#document,
	        "ldp-hidden-reply-avatar",
	        `跳到 ${displayName} 的回复 #${hiddenPostNumber}`
	      );
	      avatar.dataset.readerContextHiddenPost = String(hiddenPostNumber), avatar.setAttribute("role", "listitem");
	      const source = this.#avatarSource?.(
	        String(identity?.avatar_template ?? ""),
	        24
	      ) ?? "";
	      if (source) {
	        const image = this.#document.createElement("img");
	        image.src = source, image.alt = "", image.loading = "lazy", image.decoding = "async", avatar.append(image);
	      } else
	        avatar.textContent = [...displayName][0] ?? "?";
	      list.append(avatar);
	    }
	    if (list.dataset.readerContextHiddenRendered = String(end), end >= postNumbers.length) return;
	    const remaining = postNumbers.length - end, nextBatchSize = Math.min(
	      remaining,
	      HIDDEN_REPLY_MATERIALIZE_BATCH_SIZE
	    ), more = button(
	      this.#document,
	      "ldp-btn ldp-hidden-reply-more",
	      `再显示 ${nextBatchSize} 个隐藏回复,剩余 ${remaining} 个`,
	      `+${nextBatchSize}`
	    );
	    more.dataset.readerContextHiddenMore = "1", more.setAttribute("role", "listitem"), list.append(more);
	  }
	  #removeHiddenReplyMarker(root) {
	    this.#hiddenReplyMarkers.get(root)?.remove(), this.#hiddenReplyMarkers.delete(root), root.classList.remove("ldp-before-hidden-reply-marker");
	  }
	  async #handleClick(event, root, rootPostNumber) {
	    const target = (0, import_event_target.eventElement)(event);
	    if (!target) return;
	    const postRoot = target.closest(".ldp-post"), sourcePostNumber = postRoot && root.contains(postRoot) ? (0, import_identifiers.discoursePostNumber)(postRoot.dataset.postNumber) : rootPostNumber, collapseReply = target.closest(
	      "[data-reader-context-collapse-reply]"
	    );
	    if (collapseReply && root.contains(collapseReply) && postRoot) {
	      event.preventDefault(), this.#setRootReplyCollapsed(
	        postRoot,
	        sourcePostNumber,
	        !postRoot.classList.contains("ldp-nested-collapsed")
	      );
	      return;
	    }
	    const hiddenToggle = target.closest(
	      "[data-reader-context-hidden-toggle]"
	    ), hiddenMarker = this.#hiddenReplyMarkers.get(root);
	    if (hiddenToggle && hiddenMarker?.contains(hiddenToggle)) {
	      event.preventDefault();
	      const list = hiddenToggle.closest(
	        "[data-reader-context-hidden-replies]"
	      )?.querySelector(
	        "[data-reader-context-hidden-list]"
	      );
	      if (!list) return;
	      const expanded2 = list.hidden;
	      expanded2 && list.dataset.readerContextHiddenRendered === "0" && this.#appendHiddenReplyBatch(hiddenMarker, list), list.hidden = !expanded2, hiddenToggle.setAttribute("aria-expanded", String(expanded2));
	      const hiddenPostCount = this.#hiddenReplyPostNumbers.get(hiddenMarker)?.length ?? 0;
	      hiddenToggle.setAttribute(
	        "aria-label",
	        `${expanded2 ? "收起" : "查看"} ${hiddenPostCount} 个隐藏回复`
	      );
	      return;
	    }
	    const hiddenMore = target.closest(
	      "[data-reader-context-hidden-more]"
	    );
	    if (hiddenMore && hiddenMarker?.contains(hiddenMore)) {
	      event.preventDefault();
	      const list = hiddenMore.closest(
	        "[data-reader-context-hidden-list]"
	      );
	      list && this.#appendHiddenReplyBatch(hiddenMarker, list);
	      return;
	    }
	    const hiddenReply = target.closest(
	      "[data-reader-context-hidden-post]"
	    );
	    if (hiddenReply && hiddenMarker?.contains(hiddenReply)) {
	      event.preventDefault();
	      const navigation = this.#navigate();
	      if (!navigation) throw new Error("隐藏楼层跳转时 navigation 尚未就绪");
	      await navigation.navigate({
	        postNumber: (0, import_identifiers.discoursePostNumber)(
	          hiddenReply.dataset.readerContextHiddenPost
	        ),
	        source: "timeline",
	        alignment: "nearest",
	        highlight: !0
	      });
	      return;
	    }
	    const selfButton = target.closest(
	      "[data-reader-context-self]"
	    );
	    if (selfButton && root.contains(selfButton)) {
	      event.preventDefault();
	      const navigation = this.#navigate();
	      if (!navigation) throw new Error("楼层跳转时 navigation 尚未就绪");
	      await navigation.navigate({
	        postNumber: (0, import_identifiers.discoursePostNumber)(
	          selfButton.dataset.targetPostNumber
	        ),
	        source: "link",
	        alignment: "start",
	        highlight: !0
	      });
	      return;
	    }
	    const discussionButton = target.closest(
	      "[data-reader-context-discussion]"
	    );
	    if (discussionButton && root.contains(discussionButton)) {
	      event.preventDefault(), await this.#controller.openDiscussion(sourcePostNumber);
	      return;
	    }
	    const revealBranchButton = target.closest(
	      "[data-reader-context-reveal-branch]"
	    );
	    if (revealBranchButton && root.contains(revealBranchButton)) {
	      event.preventDefault(), this.#onRevealNextReplyLevel?.(sourcePostNumber);
	      return;
	    }
	    const branchDiscussionButton = target.closest(
	      "[data-reader-context-branch-discussion]"
	    );
	    if (branchDiscussionButton && root.contains(branchDiscussionButton)) {
	      event.preventDefault(), await this.#controller.openDiscussion(sourcePostNumber, {
	        descendantRootPostNumber: sourcePostNumber,
	        targetPostNumber: (0, import_identifiers.discoursePostNumber)(
	          branchDiscussionButton.dataset.targetPostNumber ?? sourcePostNumber
	        )
	      });
	      return;
	    }
	    const quoteAction = target.closest(
	      "[data-reader-context-quote]"
	    );
	    if (!quoteAction || !root.contains(quoteAction)) return;
	    event.preventDefault(), event.stopPropagation();
	    const targetPostNumber = (0, import_identifiers.discoursePostNumber)(
	      quoteAction.dataset.targetPostNumber
	    );
	    if (quoteAction.dataset.readerContextQuote === "jump") {
	      const targetTopicId = Number(
	        quoteAction.dataset.targetTopicId ?? this.#controller.topicId
	      ), quote2 = quoteAction.closest(".ldp-post-quote"), excerptHtml = quote2 ? this.#quoteJumpExcerptByElement.get(quote2) ?? "" : "", excerpt = quoteExcerptText(this.#document, excerptHtml), rawParentPostNumber = this.#replies.topology.parentOf(sourcePostNumber), parentPostNumber = rawParentPostNumber == null ? null : (0, import_identifiers.discoursePostNumber)(rawParentPostNumber), source = Object.freeze({
	        topicId: this.#controller.topicId,
	        postNumber: sourcePostNumber,
	        parentPostNumber,
	        nested: parentPostNumber !== null && parentPostNumber > 1,
	        anchor: this.#quoteSourcePort?.captureAnchor() ?? null
	      }), quoteHighlight = Object.freeze({
	        postNumber: targetPostNumber,
	        text: excerpt,
	        active: !0,
	        source
	      });
	      if (targetTopicId !== this.#controller.topicId) {
	        if (!this.#target)
	          throw new Error("跨主题引用跳转时 target 尚未就绪");
	        await this.#target.open({
	          topicId: targetTopicId,
	          postNumber: targetPostNumber,
	          source: "quote",
	          alignment: "nearest",
	          highlight: !1,
	          forceRefresh: !0,
	          quoteHighlight
	        });
	        return;
	      }
	      const navigation = this.#navigate();
	      if (!navigation) throw new Error("引用跳转时 navigation 尚未就绪");
	      const jumpEpoch = ++this.#quoteJumpEpoch, result = await this.#navigateQuoteWithRetry(navigation, {
	        postNumber: targetPostNumber,
	        source: "quote",
	        alignment: "nearest",
	        highlight: !1
	      }, jumpEpoch);
	      if (!result || !this.#isQuoteJumpCurrent(jumpEpoch)) return;
	      if (result.status !== "revealed") {
	        result.status === "unavailable" ? this.#notify(
	          `目的地楼层 #${targetPostNumber} 不存在或当前不可访问`
	        ) : result.status === "superseded" ? this.#notify("楼层跳转已取消;检测到新的定位或滚动操作") : result.status === "unresolved-tree" ? this.#notify(
	          `目的地楼层 #${targetPostNumber} 的回复树暂未完成挂载;请稍后重试`
	        ) : this.#notify(`目的地楼层 #${targetPostNumber} 定位失败`);
	        return;
	      }
	      this.applyRevealedQuoteHighlight(
	        quoteHighlight,
	        result.element ?? null
	      ) || this.#notify(
	        `目的地内容已修改;已定位到楼层 #${targetPostNumber}`
	      );
	      return;
	    }
	    const quote = quoteAction.closest(".ldp-post-quote"), body = quote?.querySelector(":scope > blockquote");
	    if (!quote || !body || quoteAction.getAttribute("aria-busy") === "true") return;
	    const key = String(quoteAction.dataset.quoteKey ?? ""), expanded = this.#expandedQuoteKeys.has(key), anchorRoot = postRoot ?? root;
	    if (expanded)
	      this.#expandedQuoteKeys.delete(key), this.#restoreQuoteExcerpt(quote, body), quote.dataset.ldpQuoteExpanded = "0", quote.classList.remove("ldp-quote-expanded"), quoteAction.setAttribute("aria-expanded", "false"), quoteAction.setAttribute("aria-label", "展开完整引用"), quoteAction.replaceChildren(this.#icon("chevron-down")), this.#notifyQuoteBodyChanged(anchorRoot, "collapsed");
	    else {
	      quoteAction.setAttribute("aria-busy", "true");
	      const targetTopicId = Number(
	        quoteAction.dataset.targetTopicId ?? this.#controller.topicId
	      );
	      try {
	        const fullPost = await this.#loadQuotePost(
	          targetTopicId,
	          targetPostNumber
	        );
	        if (!this.#isActiveRoot(root) || !quoteAction.isConnected || !body.isConnected)
	          return;
	        if (!fullPost) {
	          quoteAction.setAttribute(
	            "aria-label",
	            "完整引用不可用;可跳到被引用楼层"
	          ), this.#notify(
	            `被引用楼层 #${targetPostNumber} 的完整正文暂不可用`
	          );
	          return;
	        }
	        this.#applyQuotePost(quote, body, fullPost), this.#expandedQuoteKeys.add(key), quote.dataset.ldpQuoteExpanded = "1", quote.classList.add("ldp-quote-expanded"), quoteAction.setAttribute("aria-expanded", "true"), quoteAction.setAttribute("aria-label", "收起引用"), quoteAction.replaceChildren(this.#icon("chevron-up")), this.#notifyQuoteBodyChanged(anchorRoot, "expanded");
	      } finally {
	        quoteAction.removeAttribute("aria-busy");
	      }
	    }
	  }
	  #isActiveRoot(root) {
	    return !this.scope.destroyed && this.#rootCleanups.has(root) && root.isConnected;
	  }
	  async #navigateQuoteWithRetry(navigation, request, jumpEpoch) {
	    for (let attempt = 0; ; attempt += 1) {
	      let result;
	      try {
	        result = await navigation.navigate(request);
	      } catch (error) {
	        throw this.#isQuoteJumpCurrent(jumpEpoch) && this.#notify(
	          `目的地楼层 #${request.postNumber} 定位失败;请稍后重试`
	        ), error;
	      }
	      if (!this.#isQuoteJumpCurrent(jumpEpoch))
	        return null;
	      if (result.status !== "unresolved-tree" || attempt >= 2)
	        return result;
	      this.#notify(
	        `目标楼层定位暂时失败,${attempt + 1} 秒后自动重试一次`
	      );
	      const navigationRevision = navigation.revision;
	      if (await this.#navigationRetryDelay((attempt + 1) * 1e3), !this.#isQuoteJumpCurrent(jumpEpoch))
	        return null;
	      if (navigationRevision !== void 0 && navigation.isCurrent && !navigation.isCurrent(navigationRevision))
	        return Object.freeze({
	          ...result,
	          status: "superseded"
	        });
	    }
	  }
	  #isQuoteJumpCurrent(jumpEpoch) {
	    return !this.scope.destroyed && jumpEpoch === this.#quoteJumpEpoch;
	  }
	  #notifyQuoteBodyChanged(root, state) {
	    if (!this.#onQuoteBodyChanged) return;
	    const view = this.#viewByRoot.get(root);
	    view && this.#onQuoteBodyChanged(view, state);
	  }
	  #applyQuoteHighlight(postRoot, text, active, source = null, revealMatch = !1) {
	    this.#clearQuoteHighlight();
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postRoot.dataset.postNumber);
	    this.#quoteHighlight = Object.freeze({
	      postNumber,
	      text,
	      source,
	      active
	    }), source && this.#syncQuoteReturnEntries(postRoot, postNumber, source);
	    const content = postRoot.querySelector(
	      ":scope > .ldp-post-body > .ldp-content"
	    );
	    if (!content) return !1;
	    const releasePositioning = this.#beginQuotePositioning(), marks = markQuoteTextMatch(this.#document, content, text);
	    if (releasePositioning(), !marks.length) return !1;
	    this.#quoteHighlightMarks = marks;
	    const lastMark = marks.at(-1);
	    lastMark?.classList.add("ldp-quote-match-end"), lastMark && (lastMark.tabIndex = 0, lastMark.setAttribute("role", "button"));
	    const hint = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-quote-highlight-hint ldp-action-surface"
	    ), toggle = button(
	      this.#document,
	      "ldp-quote-highlight-toggle",
	      active ? "关闭引用高亮" : "继续引用高亮",
	      active ? "关闭高亮" : "继续高亮"
	    );
	    hint.append(toggle);
	    let returnButton = null;
	    source && this.#quoteSourcePort && (returnButton = button(
	      this.#document,
	      "ldp-quote-highlight-return",
	      source.nested ? "返回二级回复" : "返回引用楼层",
	      source.nested ? "返回二级回复" : "返回引用楼层"
	    ), hint.append(returnButton), this.#bindQuoteReturn(returnButton, source, () => {
	      hint.classList.remove("ldp-quote-hint-visible");
	    })), this.#quoteHintHost.append(hint), this.#quoteHighlightHint = hint;
	    const setActive = (nextActive) => {
	      const current = this.#quoteHighlight;
	      if (current) {
	        this.#quoteHighlight = Object.freeze({
	          ...current,
	          active: nextActive
	        });
	        for (const currentMark of this.#quoteHighlightMarks)
	          currentMark.classList.toggle(
	            "ldp-quote-match-muted",
	            !nextActive
	          );
	        toggle.textContent = nextActive ? "关闭高亮" : "继续高亮", toggle.setAttribute(
	          "aria-label",
	          nextActive ? "关闭引用高亮" : "继续引用高亮"
	        ), lastMark?.setAttribute("aria-pressed", String(nextActive));
	      }
	    }, toggleActive = (event) => {
	      event.preventDefault(), event.stopPropagation(), setActive(!(this.#quoteHighlight?.active ?? !1));
	    }, cancelHintHide = () => {
	      this.#quoteHintHideTimer !== null && (clearTimeout(this.#quoteHintHideTimer), this.#quoteHintHideTimer = null);
	    }, showHint = (event) => {
	      if (!lastMark || this.#quoteHighlightHint !== hint) return;
	      cancelHintHide(), hint.classList.add("ldp-quote-hint-visible");
	      const markRects = this.#quoteHighlightMarks.flatMap(
	        quoteHintElementRects
	      ), markRect = markRects.length ? Object.freeze({
	        left: Math.min(...markRects.map((rect) => rect.left)),
	        top: Math.min(...markRects.map((rect) => rect.top)),
	        right: Math.max(...markRects.map((rect) => rect.right)),
	        bottom: Math.max(...markRects.map((rect) => rect.bottom)),
	        width: 0,
	        height: 0
	      }) : lastMark.getBoundingClientRect(), rootRect = this.#scrollRoot.getBoundingClientRect(), hintRect = hint.getBoundingClientRect(), view = this.#document.defaultView, measuredViewportWidth = view?.innerWidth ?? this.#document.documentElement.clientWidth, measuredViewportHeight = view?.innerHeight ?? this.#document.documentElement.clientHeight, viewportWidth = Number.isFinite(measuredViewportWidth) && measuredViewportWidth > 0 ? measuredViewportWidth : Math.max(rootRect.right, hintRect.width + 16), viewportHeight = Number.isFinite(measuredViewportHeight) && measuredViewportHeight > 0 ? measuredViewportHeight : Math.max(rootRect.bottom, hintRect.height + 16), anchorX = event && Number.isFinite(event.clientX) ? event.clientX : (markRect.left + markRect.right) / 2, edge = 8, gap = QUOTE_HINT_POINTER_GAP_PX, minLeft = edge, maxLeft = Math.max(edge, viewportWidth - hintRect.width - edge), minTop = Math.max(edge, rootRect.top + edge), maxTop = Math.max(
	        minTop,
	        Math.min(viewportHeight - edge, rootRect.bottom - edge) - hintRect.height
	      ), clampLeft = (value) => Math.max(minLeft, Math.min(value, maxLeft)), clampTop = (value) => Math.max(minTop, Math.min(value, maxTop)), above = markRect.top - hintRect.height - gap, below = markRect.bottom + gap, fitsAbove = above >= minTop, fitsBelow = below <= maxTop, aboveSpace = markRect.top - minTop, belowSpace = maxTop + hintRect.height - markRect.bottom, top = fitsAbove || !fitsBelow && aboveSpace >= belowSpace ? above : below;
	      hint.style.left = `${Math.round(clampLeft(
	        anchorX - hintRect.width / 2
	      ))}px`, hint.style.top = `${Math.round(clampTop(top))}px`;
	    }, scheduleHintHide = () => {
	      cancelHintHide(), this.#quoteHintHideTimer = setTimeout(() => {
	        this.#quoteHintHideTimer = null, !(hint.matches(":hover,:focus-within") || this.#quoteHighlightMarks.some((mark) => mark.matches(":hover"))) && hint.classList.remove("ldp-quote-hint-visible");
	      }, QUOTE_HINT_HIDE_GRACE_MS);
	    };
	    toggle.addEventListener("click", toggleActive), hint.addEventListener("mouseenter", cancelHintHide), hint.addEventListener("mouseleave", scheduleHintHide);
	    for (const mark of marks)
	      mark.addEventListener("mouseenter", (event) => showHint(event)), mark.addEventListener("mouseleave", scheduleHintHide);
	    lastMark?.addEventListener("focus", () => showHint()), lastMark?.addEventListener("blur", scheduleHintHide), lastMark?.addEventListener("keydown", (event) => {
	      (event.key === "Enter" || event.key === " ") && toggleActive(event);
	    });
	    for (const mark of marks)
	      mark.classList.toggle("ldp-quote-match-muted", !active), mark.addEventListener("click", toggleActive);
	    if (setActive(active), revealMatch) {
	      const firstMark = marks[0];
	      firstMark && this.#revealQuoteTarget?.(firstMark, "match");
	    }
	    return !0;
	  }
	  #syncQuoteReturnEntries(postRoot, postNumber, source) {
	    const roots = /* @__PURE__ */ new Set([
	      postRoot,
	      ...this.#rootsByPostNumber.get(postNumber) ?? []
	    ]);
	    for (const root of roots) this.#mountQuoteReturnEntry(root, source);
	  }
	  #mountQuoteReturnEntry(postRoot, source) {
	    if (!this.#quoteSourcePort) return;
	    const header = postRoot.querySelector(
	      ":scope > .ldp-post-head"
	    );
	    if (!header) return;
	    const existing = header.querySelector(
	      ":scope > [data-reader-context-quote-return]"
	    );
	    if (existing) {
	      this.#quoteReturnEntries.add(existing);
	      return;
	    }
	    const label = source.nested ? "返回二级回复" : "返回引用楼层", entry = button(
	      this.#document,
	      "ldp-btn ldp-quote-return-entry",
	      label,
	      "← 返回引用处"
	    );
	    entry.dataset.readerContextQuoteReturn = "1";
	    const floor = header.querySelector(
	      ":scope > :is(.ldp-body-floor,[data-reader-context-self])"
	    );
	    floor ? floor.insertAdjacentElement("afterend", entry) : header.append(entry), this.#quoteReturnEntries.add(entry), this.#bindQuoteReturn(entry, source);
	  }
	  #bindQuoteReturn(control, source, onRestored = () => {
	  }) {
	    control.addEventListener("click", (event) => {
	      event.preventDefault(), event.stopPropagation();
	      const sourcePort = this.#quoteSourcePort;
	      if (control.disabled || !sourcePort) return;
	      control.disabled = !0;
	      const original = control.textContent;
	      control.textContent = "返回中…", sourcePort.restore(source).then((restored) => {
	        if (!this.scope.destroyed) {
	          if (!restored) throw new Error("引用来源暂不可用");
	          onRestored();
	        }
	      }).catch((error) => {
	        this.scope.destroyed || this.#onError(error);
	      }).finally(() => {
	        control.isConnected && (control.disabled = !1, control.textContent = original);
	      });
	    });
	  }
	  #beginQuotePositioning() {
	    const epoch = ++this.#quotePositionEpoch;
	    return this.#scrollRoot.classList.add("ldp-quote-positioning"), () => {
	      const release = () => {
	        this.scope.destroyed || epoch !== this.#quotePositionEpoch || this.#scrollRoot.classList.remove("ldp-quote-positioning");
	      }, view = this.#document.defaultView;
	      typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(release) : queueMicrotask(release);
	    };
	  }
	  #clearQuoteHighlight() {
	    this.#quoteHintHideTimer !== null && (clearTimeout(this.#quoteHintHideTimer), this.#quoteHintHideTimer = null), this.#quoteHighlightHint?.remove(), this.#quoteHighlightHint = null;
	    for (const entry of this.#quoteReturnEntries) entry.remove();
	    this.#quoteReturnEntries.clear();
	    for (const mark of this.#quoteHighlightMarks) {
	      if (!mark.parentNode) continue;
	      const parent = mark.parentNode;
	      mark.replaceWith(this.#document.createTextNode(mark.textContent ?? "")), parent.normalize();
	    }
	    this.#quoteHighlightMarks = Object.freeze([]), this.#quoteHighlight = null;
	  }
	}
	class ReaderTopicDiscussionTopology {
	  #snapshot = null;
	  #entries = /* @__PURE__ */ new Map();
	  update(snapshot) {
	    this.#snapshot = snapshot, this.#entries.clear();
	    for (const entry of snapshot?.entries ?? [])
	      this.#entries.set(entry.postNumber, entry);
	  }
	  parentOf(postNumber) {
	    return this.#entry(postNumber)?.parentPostNumber;
	  }
	  depthOf(postNumber) {
	    return this.#entry(postNumber)?.depth;
	  }
	  rootOf(postNumber) {
	    return this.#entry(postNumber) ? this.#snapshot?.rootPostNumber : void 0;
	  }
	  #entry(postNumber) {
	    return this.#entries.get(postNumber);
	  }
	}
	class ReaderTopicContextSurface {
	  scope;
	  discussionDomOwner;
	  discussionGeometry;
	  discussionPointer;
	  discussionBranchOverlay;
	  #document;
	  #controller;
	  #state;
	  #workspace;
	  #discussionHost;
	  #postProjector;
	  #highlight;
	  #requestFrame;
	  #cancelFrame;
	  #readComputedStyle;
	  #onError;
	  #discussionTopology = new ReaderTopicDiscussionTopology();
	  #discussionLayer;
	  #discussionPanel;
	  #discussionTitle;
	  #discussionList;
	  #discussionResizeHandles;
	  #mountedDiscussionPostNumbers = /* @__PURE__ */ new Set();
	  #discussionBranchPostNumbers = /* @__PURE__ */ new Set();
	  #discussionCollapsedPostNumbers = /* @__PURE__ */ new Set();
	  #observedDiscussionContent = /* @__PURE__ */ new Map();
	  #activeDiscussionContent = /* @__PURE__ */ new Set();
	  #renderedDiscussionPostNumbers = /* @__PURE__ */ new Set();
	  #discussionMaterializedLru = /* @__PURE__ */ new Map();
	  #discussionPostsByNumber = /* @__PURE__ */ new Map();
	  #discussionEagerPostLimit;
	  #readDiscussionMaterializedPostLimit;
	  #discussionContentObserver;
	  #layoutResizeObserver;
	  #returnFocus = null;
	  #pendingRestorePoint = null;
	  #activeDiscussionRoot = null;
	  #stateLoaded = !1;
	  #treeWidthFrame = 0;
	  #discussionBranchFrame = 0;
	  #discussionContentWidth = 0;
	  #observesDiscussionSize = !1;
	  #treePan = null;
	  #suppressTreeClick = !1;
	  #lastWorkspaceFullPage;
	  get discussionContentHost() {
	    return this.#discussionLayer;
	  }
	  scrollDiscussionHorizontal(delta) {
	    return this.scope.destroyed || this.#discussionLayer.hidden || !Number.isFinite(delta) || delta === 0 ? !1 : (this.#discussionList.scrollLeft += delta, !0);
	  }
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#document = options.document, this.#controller = options.controller, this.#state = options.stateRepository ?? new import_reader_topic_context_state.ReaderTopicContextStateRepository({
	      storage: options.stateStorage ?? Object.freeze({
	        getValue: () => null,
	        setValue: () => {
	        }
	      }),
	      ...options.onError ? { onError: options.onError } : {}
	    }), this.#workspace = options.workspace, this.#discussionHost = options.discussionHost, this.#postProjector = options.postProjector ?? new import_reader_post_view_projector.ReaderPostViewProjector({
	      document: options.document,
	      identity: options.identity,
	      render: options.renderPost,
	      ...options.postFeatures ? { features: options.postFeatures } : {},
	      ...options.onError ? { onError: options.onError } : {}
	    }), this.#highlight = options.highlight ?? (() => {
	    }), this.#requestFrame = options.requestFrame ?? (typeof requestAnimationFrame == "function" ? (callback) => requestAnimationFrame(callback) : (callback) => (callback(0), 0)), this.#cancelFrame = options.cancelFrame ?? (typeof cancelAnimationFrame == "function" ? (id) => cancelAnimationFrame(id) : () => {
	    }), this.#readComputedStyle = options.readComputedStyle, this.#onError = options.onError ?? (() => {
	    }), this.#discussionEagerPostLimit = Number.isFinite(
	      options.discussionEagerPostLimit
	    ) ? Math.max(1, Math.floor(Number(options.discussionEagerPostLimit))) : 12, this.#readDiscussionMaterializedPostLimit = options.readDiscussionMaterializedPostLimit ?? (() => Math.max(24, this.#discussionEagerPostLimit * 3)), this.#discussionLayer = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-descendant-replies-layer"
	    ), this.#discussionLayer.hidden = !0, this.#discussionPanel = (0, import_html_element.htmlElement)(
	      options.document,
	      "section",
	      "ldp-descendant-replies-window"
	    );
	    const header = (0, import_html_element.htmlElement)(
	      options.document,
	      "header",
	      "ldp-descendant-replies-header"
	    );
	    header.dataset.readerContextDragSurface = "1";
	    const close = button(
	      options.document,
	      "ldp-btn ldp-author ldp-descendant-replies-close",
	      "关闭完整讨论(Esc)",
	      "<"
	    );
	    this.#discussionTitle = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-author ldp-descendant-replies-title"
	    );
	    const top = button(
	      options.document,
	      "ldp-btn ldp-descendant-replies-top",
	      "回到完整讨论顶部",
	      "↑"
	    );
	    header.append(close, this.#discussionTitle, top), this.#discussionList = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-descendant-replies-list ldp-comments ldp-descendant-replies-tree"
	    ), this.#discussionList.scrollTop = 0, this.#discussionList.scrollLeft = 0;
	    const resizeDirections = Object.freeze([
	      "n",
	      "s",
	      "e",
	      "w",
	      "ne",
	      "nw",
	      "se",
	      "sw"
	    ]);
	    this.#discussionResizeHandles = Object.freeze(
	      resizeDirections.map((direction) => {
	        const handle = (0, import_html_element.htmlElement)(
	          options.document,
	          "span",
	          "ldp-descendant-replies-resize-handle"
	        );
	        return handle.dataset.readerResize = direction, handle.dataset.resize = direction, handle.setAttribute("aria-hidden", "true"), handle;
	      })
	    ), this.#discussionPanel.append(
	      header,
	      this.#discussionList,
	      ...this.#discussionResizeHandles
	    ), this.#discussionLayer.append(this.#discussionPanel), options.discussionHost.append(this.#discussionLayer), this.scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(this.#discussionLayer)), this.#discussionTopology.update(null), this.discussionDomOwner = new import_reply_tree_dom_owner.ReplyTreeDomOwner(
	      this.#discussionTopology,
	      this.#discussionList
	    ), this.discussionBranchOverlay = new import_branch_overlay.ReaderBranchOverlayController({
	      domOwner: this.discussionDomOwner,
	      renderMode: "segmented-css",
	      onToggleBranch: (postNumber) => {
	        this.#controller.toggleDiscussionBranch(postNumber);
	      },
	      readCollapsed: (postNumber) => this.#discussionCollapsedPostNumbers.has(postNumber),
	      onLayoutChange: () => this.#scheduleDiscussionBranchPaint(),
	      parentScope: this.scope
	    });
	    const createContentObserver = options.createContentObserver ?? (options.document.defaultView?.IntersectionObserver ? (callback, init) => new options.document.defaultView.IntersectionObserver(callback, init) : null);
	    this.#discussionContentObserver = createContentObserver ? createContentObserver(
	      (entries) => this.#onDiscussionContentIntersection(entries),
	      {
	        root: this.#discussionList,
	        rootMargin: "100% 0px 100% 0px",
	        threshold: 0
	      }
	    ) : null, this.scope.add(() => this.#discussionContentObserver?.disconnect()), this.#lastWorkspaceFullPage = options.workspace.snapshot.presentation.fullPage;
	    const defaultView = options.document.defaultView, readViewport = options.readViewport ?? (() => Object.freeze({
	      width: Math.max(
	        1,
	        Number(defaultView?.innerWidth) || options.discussionHost.clientWidth || 1
	      ),
	      height: Math.max(
	        1,
	        Number(defaultView?.innerHeight) || options.discussionHost.clientHeight || 1
	      )
	    })), viewport = readViewport(), viewportTarget = options.viewportTarget ?? defaultView;
	    this.discussionGeometry = new import_reader_workspace.ReaderWindowGeometryModel({
	      preferences: {
	        readerWindowWidth: 0,
	        readerWindowHeight: 0,
	        readerWindowX: 0,
	        readerWindowY: 0,
	        readerWindowLocked: !1,
	        readerWindowPinned: !1
	      },
	      viewportWidth: viewport.width,
	      viewportHeight: viewport.height,
	      mode: this.#lastWorkspaceFullPage ? "floating" : "fullpage",
	      policy: {
	        margin: 16,
	        minWidth: 320,
	        minHeight: 240,
	        compactWidth: 0,
	        defaultWidth: Math.max(1, Math.min(960, viewport.width - 48)),
	        defaultHeight: 720,
	        defaultViewportWidth: 1,
	        defaultViewportHeight: 0.78
	      }
	    }), this.discussionGeometry.changes.subscribe(
	      (snapshot) => this.#applyDiscussionGeometry(snapshot),
	      this.scope
	    ), this.discussionPointer = new import_reader_workspace.ReaderWindowPointerController({
	      model: this.discussionGeometry,
	      overlay: this.#discussionLayer,
	      modal: this.#discussionPanel,
	      header,
	      ...viewportTarget ? { viewportTarget } : {},
	      readViewport,
	      onPersist: () => {
	        this.discussionGeometry.snapshot.managed && this.#state.rememberGeometry(
	          this.discussionGeometry.snapshot.geometry
	        );
	      },
	      requestFrame: this.#requestFrame,
	      cancelFrame: this.#cancelFrame,
	      dragSurfaceSelector: ".ldp-descendant-replies-header[data-reader-context-drag-surface]",
	      interactingClassName: "ldp-descendant-replies-interacting",
	      restingTransform: "none",
	      projectPlacement: () => {
	      },
	      parentScope: this.scope
	    }), this.#syncWorkspaceMode(this.#lastWorkspaceFullPage), this.#applyDiscussionGeometry(this.discussionGeometry.snapshot), this.#discussionLayer.addEventListener("click", (event) => {
	      const target = (0, import_event_target.eventElement)(event);
	      if (target) {
	        if (target === this.#discussionLayer || target.closest(".ldp-descendant-replies-close")) {
	          event.preventDefault(), this.#controller.closeDiscussion();
	          return;
	        }
	        if (target.closest(".ldp-descendant-replies-top")) {
	          event.preventDefault(), this.#discussionList.scrollTop = 0, this.#discussionList.scrollLeft = 0;
	          return;
	        }
	      }
	    }), this.scope.listen(this.#discussionList, "pointerdown", (event) => {
	      this.#onTreePanPointerDown(event);
	    }), this.scope.listen(this.#discussionList, "pointermove", (event) => {
	      this.#onTreePanPointerMove(event);
	    });
	    for (const type of ["pointerup", "pointercancel", "lostpointercapture"])
	      this.scope.listen(this.#discussionList, type, (event) => {
	        this.#stopTreePan(event);
	      });
	    this.scope.listen(this.#discussionList, "click", (event) => {
	      this.#suppressTreeClick && (event.preventDefault(), event.stopImmediatePropagation());
	    }, !0);
	    const NativeResizeObserver = defaultView?.ResizeObserver, createResizeObserver = options.createResizeObserver ?? (NativeResizeObserver ? (callback) => new NativeResizeObserver(callback) : null);
	    this.#layoutResizeObserver = createResizeObserver ? createResizeObserver((entries) => {
	      const entry = entries.find((candidate) => candidate.target === this.#discussionList);
	      if (entry) {
	        const width = Number(entry.contentRect.width);
	        Number.isFinite(width) && width > 0 && (this.#discussionContentWidth = width), this.#applyPendingRestorePoint(), this.#scheduleDiscussionTreeWidth(), this.#scheduleDiscussionBranchPaint();
	      }
	    }) : null, this.#layoutResizeObserver && (this.#observesDiscussionSize = !0, this.#layoutResizeObserver.observe(this.#discussionList), this.scope.add(() => this.#layoutResizeObserver?.disconnect()));
	    const keydownTargets = /* @__PURE__ */ new Set([
	      options.document,
	      options.discussionHost.getRootNode()
	    ]);
	    for (const target of keydownTargets)
	      this.scope.listen(target, "keydown", (event) => {
	        this.handleEscape(event);
	      }, !0);
	    this.#controller.changes.subscribe((snapshot) => {
	      this.#project(snapshot);
	    }, this.scope), this.#workspace.changes.subscribe((snapshot) => {
	      const fullPage = snapshot.presentation.fullPage;
	      fullPage !== this.#lastWorkspaceFullPage && (this.#lastWorkspaceFullPage = fullPage, this.#controller.closeDiscussion()), this.discussionGeometry.setMode(
	        fullPage ? "floating" : "fullpage"
	      ), this.#syncWorkspaceMode(fullPage), this.#applyDiscussionGeometry(this.discussionGeometry.snapshot);
	    }, this.scope), this.scope.add(() => {
	      this.#persistActiveDiscussionPoint(), this.#treeWidthFrame && (this.#cancelFrame(this.#treeWidthFrame), this.#treeWidthFrame = 0), this.#discussionBranchFrame && (this.#cancelFrame(this.#discussionBranchFrame), this.#discussionBranchFrame = 0), this.#stopTreePan();
	      for (const postNumber of this.#mountedDiscussionPostNumbers) {
	        const root = this.discussionDomOwner.view(postNumber)?.slots.root;
	        root && this.#detachDiscussionFeatures(root, postNumber);
	      }
	      this.#mountedDiscussionPostNumbers.clear(), this.#discussionBranchPostNumbers.clear(), this.#discussionCollapsedPostNumbers.clear(), this.#observedDiscussionContent.clear(), this.#activeDiscussionContent.clear(), this.discussionDomOwner.destroy(), this.#discussionLayer.remove();
	    }), this.#state.load().then((state) => {
	      if (!this.scope.destroyed) {
	        if (this.#stateLoaded = !0, state.fullPageGeometry) {
	          const stored = state.fullPageGeometry;
	          this.discussionGeometry.setGeometry(
	            stored.width,
	            stored.height,
	            stored.left,
	            stored.top
	          );
	        }
	        this.#activeDiscussionRoot !== null && this.#pendingRestorePoint === null && (this.#pendingRestorePoint = this.#storedDiscussionPoint(
	          this.#activeDiscussionRoot
	        ), this.#applyPendingRestorePoint());
	      }
	    }).catch(this.#onError), this.#project(this.#controller.snapshot());
	  }
	  /**
	   * 消费当前完整讨论拥有的 Esc。
	   *
	   * 除了 Document/ShadowRoot 的局部监听,userscript entry 还会在 Window
	   * capture 阶段调用它,避免宿主更早的监听器截断事件传播。
	   */
	  handleEscape(event) {
	    return event.key !== "Escape" || !this.#controller.snapshot().discussion || !(0, import_reader_escape_surface.readerEscapeOwnedBy)(this.#document, this.#discussionLayer) ? !1 : (event.preventDefault(), event.stopImmediatePropagation(), this.#controller.closeDiscussion(), !0);
	  }
	  captureDiscussionState() {
	    const discussion = this.#controller.snapshot().discussion;
	    return !discussion || this.#discussionLayer.hidden ? null : Object.freeze({
	      rootPostNumber: discussion.rootPostNumber,
	      ...discussion.descendantRootPostNumber !== discussion.rootPostNumber ? {
	        descendantRootPostNumber: discussion.descendantRootPostNumber
	      } : {},
	      point: this.#captureDiscussionPoint()
	    });
	  }
	  async restoreDiscussionState(state) {
	    this.#pendingRestorePoint = state.point, await this.#controller.openDiscussion(state.rootPostNumber, {
	      explicitRoot: !0,
	      targetPostNumber: null,
	      ...state.descendantRootPostNumber === void 0 ? {} : {
	        descendantRootPostNumber: state.descendantRootPostNumber
	      }
	    }), this.#applyPendingRestorePoint();
	  }
	  /**
	   * 在完整讨论局部投影中揭示目标;不会把停放楼层提升进主信息流。
	   */
	  async revealDiscussionPost(postNumberValue) {
	    if (this.scope.destroyed)
	      throw new Error("完整讨论 surface 已销毁");
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue), wasMounted = this.discussionDomOwner.view(postNumber)?.slots.root?.isConnected === !0, snapshot = await this.#controller.openDiscussion(postNumber, {
	      targetPostNumber: postNumber
	    });
	    if (this.scope.destroyed) return null;
	    const discussion = snapshot.discussion, target = this.discussionDomOwner.view(postNumber)?.slots.root;
	    return !discussion || !target?.isConnected || target.classList.contains("ldp-post-projection-pending") ? null : Object.freeze({
	      postNumber,
	      rootPostNumber: discussion.rootPostNumber,
	      element: target,
	      mounted: !wasMounted
	    });
	  }
	  /** 只闪烁完整讨论中已挂载的楼层,不移动浮窗滚动位置。 */
	  highlightDiscussionPost(postNumberValue) {
	    if (this.scope.destroyed) return !1;
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue), target = this.discussionDomOwner.view(postNumber)?.slots.root;
	    return !target?.isConnected || target.classList.contains("ldp-post-projection-pending") ? !1 : (this.#highlight(target), !0);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #project(snapshot) {
	    this.scope.destroyed || this.#projectDiscussion(snapshot.discussion);
	  }
	  #projectDiscussion(snapshot) {
	    if (this.#discussionTopology.update(snapshot), !snapshot) {
	      this.#persistActiveDiscussionPoint(), this.#activeDiscussionRoot = null, this.#discussionLayer.hidden = !0, this.#discussionHost.classList.remove(
	        "ldp-descendant-replies-host-open"
	      ), this.#clearDiscussionViews(), this.#returnFocus?.focus?.(), this.#returnFocus = null;
	      return;
	    }
	    if (this.#discussionLayer.hidden) {
	      const active = (0, import_event_target.deepActiveElement)(this.#document);
	      this.#returnFocus = active && active.nodeType === 1 ? active : null;
	    }
	    this.#activeDiscussionRoot !== snapshot.rootPostNumber && (this.#persistActiveDiscussionPoint(), this.#activeDiscussionRoot = snapshot.rootPostNumber, this.#pendingRestorePoint === null && this.#stateLoaded && (this.#pendingRestorePoint = this.#storedDiscussionPoint(
	      snapshot.rootPostNumber
	    ))), this.#discussionLayer.hidden = !1, this.#discussionHost.classList.toggle(
	      "ldp-descendant-replies-host-open",
	      this.discussionGeometry.snapshot.managed
	    );
	    const titleEntry = snapshot.entries.find((entry) => entry.postNumber === snapshot.descendantRootPostNumber), titleIdentity = titleEntry ? this.#postProjector.identity(titleEntry.post) : null, branchMode = snapshot.descendantRootPostNumber !== snapshot.rootPostNumber, titlePostNumber = snapshot.descendantRootPostNumber, titleAction = branchMode ? "查看完整分支" : "查看完整讨论";
	    this.#discussionTitle.textContent = titleIdentity ? `#${titlePostNumber} · ${titleIdentity.username} · ${titleAction}(${snapshot.entries.length})` : `#${titlePostNumber} · ${titleAction}(${snapshot.entries.length})`, this.#discussionTitle.dataset.partial = snapshot.partial ? "1" : "0";
	    const nextPostNumbers = new Set(
	      snapshot.entries.map((entry) => entry.postNumber)
	    );
	    this.#discussionPostsByNumber.clear();
	    for (const entry of snapshot.entries)
	      this.#discussionPostsByNumber.set(entry.postNumber, entry.post);
	    const eagerPostNumbers = this.#discussionEagerPostNumbers(snapshot);
	    for (const postNumber of [...this.#mountedDiscussionPostNumbers]) {
	      if (nextPostNumbers.has(postNumber)) continue;
	      const view = this.discussionDomOwner.view(postNumber);
	      view && this.#detachDiscussionFeatures(view.slots.root, postNumber), this.discussionDomOwner.unregister(postNumber, !0, !1), this.#mountedDiscussionPostNumbers.delete(postNumber), this.#renderedDiscussionPostNumbers.delete(postNumber), this.#discussionMaterializedLru.delete(postNumber);
	    }
	    const attachAfterSync = /* @__PURE__ */ new Set();
	    for (const entry of snapshot.entries) {
	      let view = this.discussionDomOwner.view(
	        entry.postNumber
	      ), created = !1;
	      const shouldRender = eagerPostNumbers.has(entry.postNumber);
	      if (!view)
	        try {
	          view = shouldRender ? this.#postProjector.create(
	            entry.post,
	            this.scope,
	            entry.postNumber
	          ) : this.#postProjector.createShell(
	            entry.post,
	            this.scope,
	            entry.postNumber
	          ), created = !0, shouldRender ? (this.#renderedDiscussionPostNumbers.add(entry.postNumber), this.#touchDiscussionMaterialized(entry.postNumber)) : (view.slots.root.classList.add("ldp-post-projection-pending"), view.slots.root.setAttribute("aria-busy", "true")), this.discussionDomOwner.register(view, !1), this.#mountedDiscussionPostNumbers.add(entry.postNumber), attachAfterSync.add(entry.postNumber);
	        } catch (error) {
	          view?.destroy(), this.#onError(error);
	          continue;
	        }
	      if (!created && shouldRender && !this.#renderedDiscussionPostNumbers.has(entry.postNumber))
	        this.#materializeDiscussionView(entry.postNumber);
	      else if (!created && this.#renderedDiscussionPostNumbers.has(entry.postNumber))
	        try {
	          this.#postProjector.render(entry.post, view);
	        } catch (error) {
	          this.#onError(error);
	        }
	      view.slots.root.classList.remove("ldp-nested-collapsed"), view.slots.root.classList.add(
	        entry.depth > 0 ? "ldp-nested-preview" : "ldp-discussion-root"
	      ), view.slots.root.isConnected || attachAfterSync.add(entry.postNumber);
	    }
	    this.discussionDomOwner.sync();
	    for (const postNumber of attachAfterSync) {
	      const rootElement = this.discussionDomOwner.view(postNumber)?.slots.root;
	      rootElement?.isConnected && this.#attachDiscussionFeatures(rootElement, postNumber);
	    }
	    this.#syncDiscussionBranches(snapshot), this.#scheduleDiscussionTreeWidth(), this.#scheduleDiscussionBranchPaint(), this.#applyPendingRestorePoint();
	    const targetPostNumber = snapshot.targetPostNumber;
	    if (targetPostNumber !== null) {
	      const target = this.discussionDomOwner.view(targetPostNumber)?.slots.root;
	      target?.isConnected && (this.#highlight(target), target.scrollIntoView?.({ block: "center", inline: "nearest" }), this.#requestFrame(() => {
	        this.scope.destroyed || this.#controller.clearDiscussionTarget();
	      }));
	    }
	  }
	  #syncDiscussionBranches(snapshot) {
	    const collapsed = new Set(snapshot.collapsedPostNumbers);
	    this.#discussionCollapsedPostNumbers.clear();
	    for (const postNumber of collapsed)
	      this.#discussionCollapsedPostNumbers.add(postNumber);
	    for (const entry of snapshot.entries) {
	      const view = this.discussionDomOwner.view(entry.postNumber);
	      view && (view.slots.replyList.hidden = collapsed.has(entry.postNumber), view.slots.replyControls.querySelectorAll(":scope > [data-reader-context-discussion]").forEach((control) => control.remove()));
	    }
	  }
	  #attachDiscussionFeatures(root, postNumber) {
	    this.#discussionContentObserver ? this.#observedDiscussionContent.get(postNumber) !== root && (this.#observedDiscussionContent.set(postNumber, root), this.#discussionContentObserver.observe(root)) : this.#activateDiscussionContent(root, postNumber), this.#renderedDiscussionPostNumbers.has(postNumber) && this.#attachDiscussionBranchFeatures(root, postNumber);
	  }
	  #attachDiscussionBranchFeatures(root, postNumber) {
	    this.#discussionTopology.parentOf(postNumber) === null && (this.#discussionBranchPostNumbers.has(postNumber) || (this.#discussionBranchPostNumbers.add(postNumber), this.#postProjector.attach(root, postNumber, "branch")));
	  }
	  #detachDiscussionFeatures(root, postNumber) {
	    this.#observedDiscussionContent.get(postNumber) === root && (this.#discussionContentObserver?.unobserve(root), this.#observedDiscussionContent.delete(postNumber)), this.#deactivateDiscussionContent(root, postNumber), this.#discussionBranchPostNumbers.delete(postNumber) && this.#postProjector.detach(root, postNumber, "branch");
	  }
	  #onDiscussionContentIntersection(entries) {
	    for (const entry of entries) {
	      const root = entry.target, postNumber = Number(root.dataset.postNumber);
	      !Number.isSafeInteger(postNumber) || postNumber <= 0 || this.#observedDiscussionContent.get(postNumber) === root && (entry.isIntersecting || entry.intersectionRatio > 0 ? this.#activateDiscussionContent(root, postNumber) : this.#deactivateDiscussionContent(root, postNumber));
	    }
	    this.#scheduleDiscussionBranchPaint();
	  }
	  #scheduleDiscussionBranchPaint() {
	    if (this.scope.destroyed || this.#discussionBranchFrame) return;
	    this.#discussionList.classList.add("ldp-branch-paint-pending");
	    let completed = !1;
	    const handle = this.#requestFrame(() => {
	      if (completed = !0, this.#discussionBranchFrame = 0, this.scope.destroyed || this.#discussionLayer.hidden) {
	        this.#discussionList.classList.remove(
	          "ldp-branch-paint-pending"
	        );
	        return;
	      }
	      this.discussionBranchOverlay.paint(), this.#discussionList.classList.remove("ldp-branch-paint-pending");
	    });
	    completed || (this.#discussionBranchFrame = handle);
	  }
	  #activateDiscussionContent(root, postNumber) {
	    this.#activeDiscussionContent.has(postNumber) || this.#materializeDiscussionView(postNumber) && (this.#activeDiscussionContent.add(postNumber), this.#postProjector.attach(root, postNumber, "node"));
	  }
	  #deactivateDiscussionContent(root, postNumber) {
	    this.#activeDiscussionContent.delete(postNumber) && (this.#postProjector.detach(root, postNumber, "node"), this.#touchDiscussionMaterialized(postNumber), this.#evictDiscussionMaterializedViews());
	  }
	  #clearDiscussionViews() {
	    this.#treeWidthFrame && (this.#cancelFrame(this.#treeWidthFrame), this.#treeWidthFrame = 0), this.#discussionBranchFrame && (this.#cancelFrame(this.#discussionBranchFrame), this.#discussionBranchFrame = 0), this.#discussionList.classList.remove("ldp-branch-paint-pending"), this.discussionBranchOverlay.releaseProjection();
	    for (const postNumber of [...this.#mountedDiscussionPostNumbers]) {
	      const view = this.discussionDomOwner.view(postNumber);
	      view && this.#detachDiscussionFeatures(view.slots.root, postNumber), this.discussionDomOwner.unregister(postNumber, !0, !1);
	    }
	    this.#mountedDiscussionPostNumbers.clear(), this.#discussionBranchPostNumbers.clear(), this.#discussionCollapsedPostNumbers.clear(), this.#observedDiscussionContent.clear(), this.#activeDiscussionContent.clear(), this.#renderedDiscussionPostNumbers.clear(), this.#discussionMaterializedLru.clear(), this.#discussionPostsByNumber.clear(), this.#discussionList.replaceChildren();
	  }
	  #discussionEagerPostNumbers(snapshot) {
	    if (!this.#discussionContentObserver)
	      return new Set(snapshot.entries.map((entry) => entry.postNumber));
	    const eager = new Set(
	      snapshot.entries.slice(0, this.#discussionEagerPostLimit).map((entry) => entry.postNumber)
	    );
	    eager.add(snapshot.rootPostNumber);
	    for (const target of [
	      snapshot.targetPostNumber,
	      this.#pendingRestorePoint?.number ?? null
	    ]) {
	      let cursor = target;
	      const visited = /* @__PURE__ */ new Set();
	      for (; cursor != null && !visited.has(cursor); ) {
	        visited.add(cursor), eager.add(cursor);
	        const parent = this.#discussionTopology.parentOf(cursor);
	        cursor = parent === void 0 ? null : parent;
	      }
	    }
	    return eager;
	  }
	  #materializeDiscussionView(postNumber) {
	    if (this.#renderedDiscussionPostNumbers.has(postNumber))
	      return this.#touchDiscussionMaterialized(postNumber), !0;
	    const post = this.#discussionPostsByNumber.get(postNumber), view = this.discussionDomOwner.view(postNumber);
	    if (!post || !view) return !1;
	    try {
	      this.#postProjector.render(post, view);
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	    return this.#renderedDiscussionPostNumbers.add(postNumber), this.#touchDiscussionMaterialized(postNumber), view.slots.root.classList.remove("ldp-post-projection-pending"), view.slots.root.removeAttribute("aria-busy"), view.slots.root.isConnected && this.#attachDiscussionBranchFeatures(view.slots.root, postNumber), this.#evictDiscussionMaterializedViews(postNumber), !0;
	  }
	  #touchDiscussionMaterialized(postNumber) {
	    this.#renderedDiscussionPostNumbers.has(postNumber) && (this.#discussionMaterializedLru.delete(postNumber), this.#discussionMaterializedLru.set(postNumber, !0));
	  }
	  #discussionMaterializedPostLimit() {
	    let configured = Number.NaN;
	    try {
	      configured = Number(this.#readDiscussionMaterializedPostLimit());
	    } catch (error) {
	      this.#onError(error);
	    }
	    return Math.max(
	      this.#discussionEagerPostLimit,
	      Number.isFinite(configured) ? Math.floor(configured) : 24
	    );
	  }
	  #evictDiscussionMaterializedViews(protectedPostNumber) {
	    const limit = this.#discussionMaterializedPostLimit();
	    if (!(this.#renderedDiscussionPostNumbers.size <= limit))
	      for (const postNumber of [...this.#discussionMaterializedLru.keys()]) {
	        if (this.#renderedDiscussionPostNumbers.size <= limit) break;
	        postNumber === protectedPostNumber || postNumber === this.#activeDiscussionRoot || this.#activeDiscussionContent.has(postNumber) || this.#replaceDiscussionViewWithShell(postNumber);
	      }
	  }
	  #replaceDiscussionViewWithShell(postNumber) {
	    if (!this.#renderedDiscussionPostNumbers.has(postNumber) || this.#activeDiscussionContent.has(postNumber))
	      return !1;
	    const post = this.#discussionPostsByNumber.get(postNumber), current = this.discussionDomOwner.view(postNumber);
	    if (!post || !current) return !1;
	    let shell;
	    try {
	      shell = this.#postProjector.createShell(
	        post,
	        this.scope,
	        postNumber
	      );
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	    shell.slots.root.classList.add("ldp-post-projection-pending"), shell.slots.root.setAttribute("aria-busy", "true");
	    const depth = this.#discussionTopology.depthOf(postNumber);
	    shell.slots.root.classList.add(
	      depth !== void 0 && depth > 0 ? "ldp-nested-preview" : "ldp-discussion-root"
	    ), shell.slots.root.classList.toggle(
	      "ldp-nested-collapsed",
	      current.slots.root.classList.contains("ldp-nested-collapsed")
	    ), shell.slots.replyList.hidden = current.slots.replyList.hidden, this.#detachDiscussionFeatures(current.slots.root, postNumber);
	    try {
	      this.discussionDomOwner.unregister(postNumber, !1, !1), this.discussionDomOwner.register(shell, !1), this.discussionDomOwner.sync();
	    } catch (error) {
	      this.discussionDomOwner.unregister(postNumber, !1, !1), shell.destroy();
	      try {
	        this.discussionDomOwner.register(current, !1), this.discussionDomOwner.sync(), current.slots.root.isConnected && this.#attachDiscussionFeatures(current.slots.root, postNumber);
	      } catch (rollbackError) {
	        this.#onError(rollbackError);
	      }
	      return this.#onError(error), !1;
	    }
	    return current.destroy(), this.#renderedDiscussionPostNumbers.delete(postNumber), this.#discussionMaterializedLru.delete(postNumber), shell.slots.root.isConnected && this.#attachDiscussionFeatures(shell.slots.root, postNumber), this.#scheduleDiscussionBranchPaint(), !0;
	  }
	  #captureDiscussionPoint() {
	    const listRect = this.#discussionList.getBoundingClientRect(), anchor = [...this.#discussionList.querySelectorAll(
	      ".ldp-post"
	    )].find((post) => post.getBoundingClientRect().bottom > listRect.top);
	    return anchor ? Object.freeze({
	      number: (0, import_identifiers.discoursePostNumber)(anchor.dataset.postNumber),
	      scrollTop: Math.max(0, this.#discussionList.scrollTop),
	      scrollLeft: Math.max(0, this.#discussionList.scrollLeft),
	      offset: anchor.getBoundingClientRect().top - listRect.top
	    }) : null;
	  }
	  #applyPendingRestorePoint() {
	    const point = this.#pendingRestorePoint;
	    if (!point) return;
	    const target = this.discussionDomOwner.view(point.number)?.slots.root;
	    if (!target?.isConnected) return;
	    const currentScrollTop = Math.max(0, this.#discussionList.scrollTop), targetRect = target.getBoundingClientRect(), listRect = this.#discussionList.getBoundingClientRect(), anchoredScrollTop = currentScrollTop + targetRect.top - listRect.top - point.offset;
	    this.#pendingRestorePoint = null, this.#discussionList.scrollLeft = point.scrollLeft, this.#discussionList.scrollTop = Math.max(
	      0,
	      Number.isFinite(anchoredScrollTop) ? anchoredScrollTop : point.scrollTop
	    );
	  }
	  #storedDiscussionPoint(rootPostNumber) {
	    return this.#state.point(
	      this.#document.location?.host ?? "",
	      this.#controller.topicId,
	      rootPostNumber
	    );
	  }
	  #persistActiveDiscussionPoint() {
	    this.#activeDiscussionRoot === null || this.#discussionLayer.hidden || this.#state.rememberPoint(
	      this.#document.location?.host ?? "",
	      this.#controller.topicId,
	      this.#activeDiscussionRoot,
	      this.#captureDiscussionPoint()
	    );
	  }
	  #applyDiscussionGeometry(snapshot) {
	    const managed = snapshot.managed;
	    for (const handle of this.#discussionResizeHandles)
	      handle.hidden = !managed;
	    const header = this.#discussionPanel.querySelector(
	      ":scope > .ldp-descendant-replies-header"
	    );
	    if (header && (header.style.cursor = managed ? "move" : "", header.style.touchAction = managed ? "none" : ""), managed) {
	      const value = snapshot.geometry;
	      this.#discussionPanel.style.left = `${value.left}px`, this.#discussionPanel.style.top = `${value.top}px`, this.#discussionPanel.style.width = `${value.width}px`, this.#discussionPanel.style.height = `${value.height}px`, this.#discussionPanel.style.transform = "none";
	    } else
	      for (const property of [
	        "left",
	        "top",
	        "width",
	        "height",
	        "transform"
	      ])
	        this.#discussionPanel.style.removeProperty(property);
	    this.#discussionHost.classList.toggle(
	      "ldp-descendant-replies-host-open",
	      managed && !this.#discussionLayer.hidden
	    ), this.#scheduleDiscussionTreeWidth();
	  }
	  #scheduleDiscussionTreeWidth() {
	    if (this.#treeWidthFrame) return;
	    let completed = !1;
	    const handle = this.#requestFrame(() => {
	      completed = !0, this.#treeWidthFrame = 0, this.#syncDiscussionTreeWidth();
	    });
	    completed || (this.#treeWidthFrame = handle);
	  }
	  #syncDiscussionTreeWidth() {
	    const snapshot = this.#controller.snapshot().discussion;
	    if (!snapshot || this.#discussionLayer.hidden) return;
	    let contentWidth = this.#discussionContentWidth;
	    if (contentWidth <= 0) {
	      if (this.#observesDiscussionSize) return;
	      const style = this.#readComputedStyle?.(this.#discussionList) ?? this.#document.defaultView?.getComputedStyle?.(
	        this.#discussionList
	      ), padding = Number.parseFloat(style?.paddingLeft ?? "") + Number.parseFloat(style?.paddingRight ?? ""), horizontalPadding = Number.isFinite(padding) ? padding : 0;
	      contentWidth = this.#discussionList.clientWidth - horizontalPadding;
	    }
	    const baseWidth = Math.max(320, contentWidth), maxDepth = snapshot.entries.reduce(
	      (depth, entry) => Math.max(depth, entry.depth),
	      0
	    ), visibleDepth = Math.max(
	      0,
	      Math.floor((baseWidth - 320) / 28)
	    ), overflowDepth = Math.max(0, maxDepth - visibleDepth);
	    this.#discussionList.style.setProperty(
	      "--ldp-descendant-tree-width",
	      `${baseWidth + overflowDepth * 28}px`
	    ), this.#discussionList.classList.toggle(
	      "ldp-descendant-tree-pannable",
	      overflowDepth > 0
	    ), overflowDepth || (this.#discussionList.scrollLeft = 0);
	  }
	  #onTreePanPointerDown(event) {
	    const target = (0, import_event_target.eventElement)(event);
	    if (!(event.button !== 0 || event.pointerType && event.pointerType !== "mouse" || !this.#discussionList.classList.contains(
	      "ldp-descendant-tree-pannable"
	    ) || !target || target.closest([
	      "button",
	      "a",
	      "input",
	      "select",
	      "textarea",
	      '[contenteditable="true"]',
	      '[role="button"]',
	      ".ldp-post-head",
	      ".ldp-content",
	      ".ldp-reactions",
	      ".ldp-boost-list",
	      ".ldp-sub-actions",
	      ".ldp-topic-footer-actions",
	      "img",
	      "video",
	      "audio",
	      "canvas",
	      "pre",
	      "code"
	    ].join(",")))) {
	      this.#treePan = {
	        pointerId: event.pointerId,
	        startX: event.clientX,
	        scrollLeft: this.#discussionList.scrollLeft,
	        moved: !1
	      };
	      try {
	        this.#discussionList.setPointerCapture?.(event.pointerId);
	      } catch {
	      }
	    }
	  }
	  #onTreePanPointerMove(event) {
	    const pan = this.#treePan;
	    if (!pan || event.pointerId !== pan.pointerId) return;
	    const deltaX = event.clientX - pan.startX;
	    !pan.moved && Math.abs(deltaX) < 3 || (pan.moved = !0, this.#discussionList.classList.add(
	      "ldp-descendant-tree-panning"
	    ), this.#discussionList.scrollLeft = pan.scrollLeft - deltaX, event.preventDefault());
	  }
	  #stopTreePan(event) {
	    const pan = this.#treePan;
	    if (!(!pan || event && event.pointerId !== pan.pointerId)) {
	      this.#treePan = null, this.#discussionList.classList.remove(
	        "ldp-descendant-tree-panning"
	      );
	      try {
	        this.#discussionList.hasPointerCapture?.(pan.pointerId) && this.#discussionList.releasePointerCapture?.(pan.pointerId);
	      } catch {
	      }
	      pan.moved && (this.#suppressTreeClick = !0, this.#requestFrame(() => {
	        this.#suppressTreeClick = !1;
	      }));
	    }
	  }
	  #syncWorkspaceMode(fullPage) {
	    this.#discussionLayer.classList.toggle(
	      "ldp-descendant-replies-layer-centered",
	      fullPage
	    ), this.#discussionLayer.classList.toggle(
	      "ldp-descendant-replies-layer-inline",
	      !fullPage
	    ), this.#discussionPanel.classList.toggle(
	      "ldp-descendant-replies-centered",
	      fullPage
	    ), this.#discussionPanel.classList.toggle(
	      "ldp-descendant-replies-inline",
	      !fullPage
	    );
	  }
	}
}, "07441336c4df82f46badeadd2c2a5414c10b482bff6cc11f0451df8add1627a8");

/* Source: lite/src/topic/reader-topic-core-bundle.ts */
runtime.register("src/topic/reader-topic-core-bundle.js", function(module, exports, require) {
	var reader_topic_core_bundle_exports = {};
	__export(reader_topic_core_bundle_exports, {
	  createReaderTopicCoreBundle: () => createReaderTopicCoreBundle
	});
	module.exports = __toCommonJS(reader_topic_core_bundle_exports);
	var import_topic_snapshot_repository = require("../cache/topic-snapshot-repository.js"), import_native_message_bus = require("../discourse/native-message-bus.js"), import_native_host_api = require("../discourse/native-host-api.js"), import_native_composer = require("../discourse/native-composer.js"), import_reply_tree_repository = require("../dom/reply-tree-repository.js"), import_topic_live_controller = require("../live/topic-live-controller.js"), import_discourse_native_read_transport = require("../network/discourse-native-read-transport.js"), import_action_request_adapter = require("../post/action-request-adapter.js"), import_boost_report_access_adapter = require("../post/boost-report-access-adapter.js"), import_discourse_action_transport = require("../post/discourse-action-transport.js"), import_post_action_controller = require("../post/post-action-controller.js"), import_topic_post_action_adapter = require("../post/topic-post-action-adapter.js"), import_read_state_controller = require("../reading/read-state-controller.js"), import_read_state_request_adapter = require("../reading/read-state-request-adapter.js"), import_topic_session = require("./topic-session.js"), import_topic_read_request_adapter = require("./topic-read-request-adapter.js");
	const TOPIC_SNAPSHOT_PERSISTENCE_IDLE_MS = 1500;
	function cacheSettings(kind, topicId, lifetime) {
	  return Object.freeze({
	    kind,
	    tags: Object.freeze([`topic:${topicId}`]),
	    freshForMs: lifetime.freshForMs,
	    retainForMs: lifetime.retainForMs,
	    persist: lifetime.persist
	  });
	}
	function topicReadCaches(topicId, options) {
	  return Object.freeze({
	    topic: cacheSettings("discourse-topic-json", topicId, options.topic),
	    posts: cacheSettings("discourse-topic-posts", topicId, options.posts),
	    nested: cacheSettings("discourse-topic-replies", topicId, options.nested)
	  });
	}
	function readCandidate(post) {
	  const postNumber = Number(post.post_number);
	  if (!Number.isSafeInteger(postNumber) || postNumber < 1) return null;
	  const read = post.read;
	  return Object.freeze({
	    postNumber,
	    ...read === !0 ? { read: !0 } : {}
	  });
	}
	function createReaderTopicCoreBundle(context, options) {
	  const topicId = Number(context.topicId), report = (phase, cause) => {
	    options.onDiagnostic?.(Object.freeze({ phase, topicId, cause }));
	  }, nativeOptions = options.origin === void 0 ? {} : { origin: options.origin }, nativeAjax = options.nativeAjax ?? new import_discourse_native_read_transport.BrowserDiscourseNativeAjaxPort(
	    options.host,
	    nativeOptions
	  ), readTransport = new import_discourse_native_read_transport.BrowserDiscourseNativeReadTransport(nativeAjax), mutationTransport = new import_discourse_native_read_transport.BrowserDiscourseNativeMutationTransport(nativeAjax), topicReadController = context.scope.abortController(
	    new DOMException(`Topic ${topicId} 读取链已结束`, "AbortError"),
	    context.signal
	  ), requests = new import_topic_read_request_adapter.TopicReadRequestAdapter({
	    gateway: options.gateway,
	    transport: readTransport,
	    authScope: options.authScope,
	    topicId,
	    signal: topicReadController.signal,
	    caches: topicReadCaches(topicId, options.caches),
	    ...options.basePath === void 0 ? {} : { basePath: options.basePath }
	  }), snapshots = new import_topic_snapshot_repository.TopicSnapshotRepository({
	    responseRepository: options.responses,
	    topicId,
	    authScope: options.authScope,
	    freshForMs: options.caches.snapshot.freshForMs,
	    retainForMs: options.caches.snapshot.retainForMs,
	    persistenceIdleMs: TOPIC_SNAPSHOT_PERSISTENCE_IDLE_MS,
	    ...options.now === void 0 ? {} : { now: options.now },
	    onInvalidSnapshot: (cause) => report("snapshot", cause),
	    onInvalidTreeSnapshot: (cause) => report("reply-tree", cause)
	  }), replies = new import_reply_tree_repository.ReplyTreeRepository(
	    topicId,
	    snapshots.replyTreeSnapshotStore(),
	    {
	      ...options.now === void 0 ? {} : { now: options.now },
	      onPersistenceError: (cause) => report("reply-tree", cause)
	    }
	  ), session = new import_topic_session.TopicSession({
	    topicId,
	    requests,
	    snapshots,
	    replies,
	    pageSize: options.pageSize,
	    signal: topicReadController.signal,
	    ...options.refreshCachedInBackground === void 0 ? {} : { refreshCachedInBackground: options.refreshCachedInBackground },
	    ...options.now === void 0 ? {} : { now: options.now },
	    scope: context.scope,
	    onError: (cause) => report("session", cause),
	    ...options.onLoadingSource === void 0 ? {} : { onInitializeSource: options.onLoadingSource }
	  }), messageBus = new import_native_message_bus.BrowserDiscourseMessageBusPort(options.host), live = new import_topic_live_controller.TopicLiveController({
	    topicId,
	    messageBus,
	    session,
	    cache: options.responses,
	    currentUsername: (0, import_native_host_api.discourseNativeCurrentUsername)(options.host),
	    ...options.livePostDelayMs === void 0 ? {} : { postDelayMs: options.livePostDelayMs },
	    ...options.liveTopicDelayMs === void 0 ? {} : { topicDelayMs: options.liveTopicDelayMs },
	    scope: context.scope,
	    onError: (cause) => report("message-bus", cause)
	  }), composerEvents = new import_native_composer.DiscourseComposerTopicSyncController({
	    topicId,
	    events: options.composerEvents ?? new import_native_composer.DiscourseComposerEventPort(options.host),
	    session,
	    parentScope: context.scope,
	    ...options.now === void 0 ? {} : { now: options.now },
	    onError: (cause) => report("composer-events", cause)
	  }), readRequests = new import_read_state_request_adapter.ReadStateRequestAdapter({
	    gateway: options.gateway,
	    transport: mutationTransport,
	    authScope: options.authScope,
	    topicId,
	    signal: context.signal,
	    ...options.basePath === void 0 ? {} : { basePath: options.basePath },
	    ...options.readTimeMs === void 0 ? {} : { readTimeMs: options.readTimeMs }
	  }), read = new import_read_state_controller.ReadStateController({
	    authScope: options.authScope,
	    topicId,
	    submitter: readRequests,
	    ...options.readCoordination === void 0 ? {} : { coordination: options.readCoordination },
	    ...options.readBatchSize === void 0 ? {} : { batchSize: options.readBatchSize },
	    ...options.readRetryDelayMs === void 0 ? {} : { retryDelayMs: options.readRetryDelayMs },
	    ...options.readMaxAutomaticRetries === void 0 ? {} : { maxAutomaticRetries: options.readMaxAutomaticRetries },
	    scope: context.scope,
	    onError: (cause) => report("read-state", cause)
	  }), actionRequests = new import_action_request_adapter.ActionRequestAdapter({
	    gateway: options.gateway,
	    nativeActions: options.nativeActions ?? new import_discourse_action_transport.BrowserDiscourseNativeActionPort(options.host, nativeAjax),
	    authScope: options.authScope,
	    signal: context.signal
	  }), boostReportAccess = new import_boost_report_access_adapter.BoostReportAccessAdapter({
	    gateway: options.gateway,
	    transport: readTransport,
	    authScope: options.authScope,
	    signal: context.signal,
	    ...options.basePath === void 0 ? {} : { basePath: options.basePath }
	  }), actions = new import_post_action_controller.PostActionController({
	    mutation: actionRequests,
	    cache: options.responses,
	    scope: context.scope,
	    onError: (cause) => report("action", cause)
	  }), postActions = new import_topic_post_action_adapter.TopicPostActionAdapter({
	    session,
	    ...options.now === void 0 ? {} : { now: options.now }
	  }), preload = (posts) => {
	    const candidates = posts.map(readCandidate).filter((candidate) => candidate !== null);
	    candidates.length && read.preload(candidates);
	  };
	  session.changes.subscribe((commit) => {
	    preload(
	      commit.changedPostNumbers.map((postNumber) => session.postByNumber(postNumber)).filter((post) => post !== void 0)
	    );
	  }, context.scope);
	  const services = Object.freeze({
	    requests,
	    snapshots,
	    replies,
	    session,
	    live,
	    composerEvents,
	    readRequests,
	    read,
	    actionRequests,
	    boostReportAccess,
	    actions,
	    postActions
	  });
	  let closePromise = null;
	  return Object.freeze({
	    session,
	    replies,
	    services,
	    activate: () => {
	      preload(session.cachedPosts()), live.setActive(!0, {
	        refresh: session.initializedFromCache && session.localArchiveState().topic === null
	      });
	      try {
	        composerEvents.start();
	      } catch (cause) {
	        report("composer-events", cause);
	      }
	      return read.start(), () => {
	        read.stop(), live.active && live.setActive(!1), composerEvents.stop();
	      };
	    },
	    prepareClose: (reason) => closePromise || (topicReadController.signal.aborted || topicReadController.abort(
	      new DOMException(
	        `Topic ${topicId} 已${reason === "switch" ? "切换" : "关闭"}`,
	        "AbortError"
	      )
	    ), read.stop(), live.setActive(!1), composerEvents.stop(), closePromise = (async () => {
	      const activeResults = await Promise.allSettled([
	        read.flush({ force: !0 }),
	        live.flush()
	      ]);
	      for (const result of activeResults)
	        result.status === "rejected" && report("prepare-close", result.reason);
	      try {
	        await session.flush();
	      } catch (cause) {
	        report("prepare-close", cause);
	      }
	    })(), closePromise)
	  });
	}
}, "8bf41c6e63221b26fef77731b5a672a978ff5e7bbb71e2854b269cbc3bd579e1");

/* Source: lite/src/topic/reader-topic-dom-coordinator.ts */
runtime.register("src/topic/reader-topic-dom-coordinator.js", function(module, exports, require) {
	var reader_topic_dom_coordinator_exports = {};
	__export(reader_topic_dom_coordinator_exports, {
	  ReaderTopicDomCoordinator: () => ReaderTopicDomCoordinator
	});
	module.exports = __toCommonJS(reader_topic_dom_coordinator_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_reply_tree_dom_owner = require("../dom/reply-tree-dom-owner.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_branch_overlay = require("../layout/branch-overlay.js"), import_reader_reply_tree_preferences = require("./reader-reply-tree-preferences.js"), import_reply_tree_virtual_layout_controller = require("../stream/reply-tree-virtual-layout-controller.js"), import_reply_tree_viewport_layout = require("../stream/reply-tree-viewport-layout.js"), import_virtual_root_layout = require("../stream/virtual-root-layout.js"), import_virtual_stream_dom_controller = require("../stream/virtual-stream-dom-controller.js"), import_virtual_stream_frame_controller = require("../stream/virtual-stream-frame-controller.js"), import_virtual_stream_view = require("../stream/virtual-stream-view.js"), import_reader_post_view_projector = require("./reader-post-view-projector.js"), import_reader_topic_scroll_lifecycle = require("./reader-topic-scroll-lifecycle.js");
	const DIRECT_REPLY_VISIBLE_PIPELINE_DEPTH = 3, DIRECT_REPLY_NEARBY_PIPELINE_DEPTH = 2, DIRECT_REPLY_PREFETCH_MAX_PAGES = 5, MATERIALIZATION_STEP_SCREENS = 0.5, HIDDEN_REPLY_MARKER_BLOCK_SIZE = 18, PROJECTION_HYDRATION_MIN_IDLE_MS = 120, PROJECTION_HYDRATION_BATCH_DELAY_MS = 16, PROJECTION_HYDRATION_BATCH_SIZE = 1;
	function isAbortFailure(error) {
	  return error instanceof DOMException && error.name === "AbortError" || String(error?.name ?? "") === "AbortError";
	}
	class ReaderTopicDomCoordinator {
	  scope;
	  streamView;
	  domOwner;
	  layout;
	  domController;
	  frame;
	  branchOverlay;
	  replyTreePresentation;
	  postProjector;
	  visibleRootChanges = new import_signal.Signal();
	  windowChanges = new import_signal.Signal();
	  presentationChanges;
	  #session;
	  #scroll;
	  #onError;
	  #branchFrames;
	  #directReplyPrefetchScheduler;
	  #projectionHydrationScheduler;
	  #readDirectReplyPrefetchScreens;
	  #readDirectReplyPrefetchIdleMs;
	  #readDirectReplyPrefetchConcurrency;
	  #now;
	  #scrollLifecycle;
	  #ownsPresentationChanges;
	  #rootObservers = /* @__PURE__ */ new Map();
	  #retainedViews = /* @__PURE__ */ new Map();
	  #topicDirtyRetainedPostNumbers = /* @__PURE__ */ new Set();
	  #directReplyPrefetches = /* @__PURE__ */ new Map();
	  #queuedDirectReplyPrefetches = /* @__PURE__ */ new Set();
	  #directReplyPrefetchedExpectedCounts = /* @__PURE__ */ new Map();
	  #directReplyPrefetchAttemptedExpectedCounts = /* @__PURE__ */ new Map();
	  #rootProjection;
	  #treeViewport;
	  #ownSizeObserver;
	  #ownSizeTargets = /* @__PURE__ */ new Map();
	  #ownSizeSamples = /* @__PURE__ */ new Map();
	  #pendingOwnSizePostNumbers = /* @__PURE__ */ new Set();
	  #activeBranchPostNumbers = /* @__PURE__ */ new Set();
	  #projectionHydrationFailedPostNumbers = /* @__PURE__ */ new Set();
	  #rootVirtualInsets = /* @__PURE__ */ new Map();
	  #mountedPostNumbers = /* @__PURE__ */ new Set();
	  #activeContentPostNumbers = /* @__PURE__ */ new Set();
	  #nextContentPostNumbers = /* @__PURE__ */ new Set();
	  #desiredContentPostNumbers = /* @__PURE__ */ new Set();
	  #directReplyPrefetchCandidatePostNumbers = /* @__PURE__ */ new Set();
	  #directReplyPrefetchOrderedCandidates = Object.freeze([]);
	  #directReplyVisiblePostNumbers = /* @__PURE__ */ new Set();
	  #directReplyVisibleKey = "";
	  #directReplyPrefetchCandidateKey = "";
	  #branchPaintHandle = null;
	  #branchPaintGeneration = 0;
	  #canonicalFreezeEpoch = 0;
	  #pendingBranchCollapseAnchor = null;
	  #directReplyPrefetchHandle = null;
	  #projectionHydrationHandle = null;
	  #projectionHydrationPostNumbers = Object.freeze([]);
	  #retainedViewLimit = 0;
	  #lastVisibleRootChangeKey = "";
	  #lastPostStreamRevision;
	  #pendingViewportMutation = null;
	  #destroyed = !1;
	  constructor(options) {
	    if (this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#session = options.session, this.#scroll = options.scroll, this.#lastPostStreamRevision = options.session.postStreamRevision ?? 0, this.#onError = options.onError ?? (() => {
	    }), this.presentationChanges = options.presentationChanges ?? new import_signal.Signal(), this.#ownsPresentationChanges = options.presentationChanges === void 0, this.postProjector = new import_reader_post_view_projector.ReaderPostViewProjector({
	      document: options.document,
	      identity: options.identity,
	      render: options.render,
	      ...options.postFeatures ? { features: options.postFeatures } : {},
	      onError: this.#onError
	    }), this.#branchFrames = options.frameScheduler ?? Object.freeze({
	      request: (callback) => requestAnimationFrame(callback),
	      cancel: (handle) => cancelAnimationFrame(handle)
	    }), this.#directReplyPrefetchScheduler = options.directReplyPrefetchScheduler ?? Object.freeze({
	      schedule: (callback, delayMs) => setTimeout(callback, delayMs),
	      cancel: (handle) => clearTimeout(handle)
	    }), this.#projectionHydrationScheduler = options.projectionHydrationScheduler ?? Object.freeze({
	      schedule: (callback, delayMs) => setTimeout(callback, delayMs),
	      cancel: (handle) => clearTimeout(handle)
	    }), this.#readDirectReplyPrefetchScreens = options.readDirectReplyPrefetchScreens ?? (() => 0), this.#readDirectReplyPrefetchIdleMs = options.readDirectReplyPrefetchIdleMs ?? (() => 180), this.#readDirectReplyPrefetchConcurrency = options.readDirectReplyPrefetchConcurrency ?? (() => 1), this.#now = options.now ?? (() => performance.now()), this.#scrollLifecycle = new import_reader_topic_scroll_lifecycle.ReaderTopicScrollLifecycle({
	      readLastUserScrollAt: () => options.scroll.lastUserScrollAt?.() ?? 0,
	      readIdleMs: this.#readDirectReplyPrefetchIdleMs,
	      scheduler: this.#directReplyPrefetchScheduler,
	      now: this.#now,
	      parentScope: this.scope
	    }), this.streamView = new import_virtual_stream_view.VirtualStreamView(options.document), options.topicHost.append(this.streamView.slots.root), this.scope.add(() => this.streamView.destroy()), options.replyTreePresentation && options.replyTreePresentation.canonical !== options.replies.topology)
	      throw new Error(
	        "外部 ReplyTree presentation 必须投影当前 canonical topology"
	      );
	    this.replyTreePresentation = options.replyTreePresentation ?? new import_reader_reply_tree_preferences.ReaderReplyTreePresentation(
	      options.replies.topology,
	      options.replyTreePreferences?.read(),
	      {
	        canonicalCoverageComplete: () => {
	          const replyCoverage = options.replies.coverage();
	          if (!replyCoverage.complete) return !1;
	          const topicExpectedPostCount = options.session.postStreamCoverage?.().expectedPostCount;
	          return topicExpectedPostCount === void 0 || replyCoverage.expectedPostCount >= topicExpectedPostCount;
	        },
	        canonicalPostStreamRevision: () => options.session.postStreamRevision ?? 0,
	        canonicalPostStreamGapCount: (postNumber, previousRootPostNumber) => options.session.postStreamGapCount?.(
	          previousRootPostNumber,
	          postNumber
	        )
	      }
	    ), this.domOwner = new import_reply_tree_dom_owner.ReplyTreeDomOwner(
	      this.replyTreePresentation,
	      this.streamView.slots.rootList
	    ), this.scope.add(() => this.domOwner.destroy()), this.scope.add(() => {
	      for (const view of this.#retainedViews.values()) view.destroy();
	      this.#retainedViews.clear(), this.#topicDirtyRetainedPostNumbers.clear();
	    }), this.layout = new import_virtual_root_layout.VirtualRootLayout(options.estimatedRootSize, !0), this.#rootProjection = new import_reply_tree_virtual_layout_controller.ReplyTreeVirtualLayoutController(
	      options.replies,
	      this.layout,
	      this.scope,
	      this.replyTreePresentation
	    ), this.#treeViewport = new import_reply_tree_viewport_layout.ReplyTreeViewportLayout(
	      this.replyTreePresentation,
	      this.layout,
	      options.estimatedRootSize
	    ), this.domController = new import_virtual_stream_dom_controller.VirtualStreamDomController(
	      options.replies,
	      this.layout,
	      this.streamView,
	      this.domOwner,
	      {
	        prepareRoots: (_postNumbers, input, window) => this.#prepareRootViews(input, window),
	        roots: () => this.replyTreePresentation.roots(),
	        resolveGapPlaceholder: (window, input) => {
	          if (window.unloadedGapTargetPostNumber !== void 0 && window.unloadedGapSide !== void 0)
	            return Object.freeze({
	              side: window.unloadedGapSide,
	              targetPostNumber: window.unloadedGapTargetPostNumber
	            });
	          const targetPostNumber = this.visibleDataGapPostNumber();
	          if (targetPostNumber === void 0) return null;
	          const targetOffset = this.#treeViewport.offsetOf(targetPostNumber);
	          return Object.freeze({
	            side: targetOffset !== void 0 && targetOffset > input.scrollOffset + input.viewportSize / 2 ? "after" : "before",
	            targetPostNumber
	          });
	        }
	      }
	    ), this.frame = new import_virtual_stream_frame_controller.VirtualStreamFrameController(this.domController, {
	      readWindowInput: () => this.#readVirtualWindowInput(),
	      applyScrollCompensation: (delta) => options.scroll.applyScrollCompensation(delta),
	      /*
	       * 根高测量只更新虚拟坐标。活跃滚动由 Chromium 独占;停稳后的物理
	       * 视野由 scroll adapter 的唯一视野锁持有,frame 不得再写第二份补偿。
	       */
	      shouldApplyScrollCompensation: () => !1,
	      shouldDeferMeasurements: () => !1,
	      resolveRootBlockSize: (target, observedBlockSize) => {
	        const postNumber = Number(target.getAttribute("data-post-number")), inset = Number.isSafeInteger(postNumber) ? this.#rootVirtualInsets.get(postNumber) : void 0, virtualBlockSize = observedBlockSize + (inset?.beforeSize ?? 0) + (inset?.afterSize ?? 0), marker = target.nextElementSibling;
	        return marker?.nodeType === 1 && marker.classList.contains("ldp-hidden-reply-marker") ? virtualBlockSize + HIDDEN_REPLY_MARKER_BLOCK_SIZE : virtualBlockSize;
	      },
	      onCommit: (commit) => {
	        for (const error of this.windowChanges.emit(commit))
	          this.#onError(error);
	        this.#syncRootObservers(
	          commit.attachedRoots,
	          commit.detachedRoots
	        );
	        const projectionChanged = commit.tree.changed !== !1;
	        projectionChanged && this.#syncProjectionFeatures(), this.#syncContentFeatures(), this.#syncDirectReplyPrefetchCandidates(), this.#releaseParkedViews(commit.tree.parked), projectionChanged && this.#scheduleBranchPaint();
	        const visiblePostNumber = this.#scroll.readVisibleViewportAnchor?.(
	          this.#visiblePostElements()
	        )?.postNumber ?? this.#treeViewport.visiblePostNumbers[0] ?? commit.window.visiblePostNumbers[0], visibleRootPostNumber = visiblePostNumber === void 0 ? void 0 : this.replyTreePresentation.rootOf(visiblePostNumber) ?? visiblePostNumber;
	        if (visibleRootPostNumber !== void 0) {
	          const changeKey = `${visibleRootPostNumber}|${Number(commit.window.atStart)}|${Number(commit.window.atEnd)}`;
	          if (changeKey !== this.#lastVisibleRootChangeKey) {
	            this.#lastVisibleRootChangeKey = changeKey;
	            for (const error of this.visibleRootChanges.emit(Object.freeze({
	              postNumber: visibleRootPostNumber,
	              atStart: commit.window.atStart,
	              atEnd: commit.window.atEnd
	            })))
	              this.#onError(error);
	          }
	        }
	        this.#restorePendingViewportMutation(), this.#scroll.notifyVirtualWindowCommit?.();
	      },
	      ...options.observerFactory ? { observerFactory: options.observerFactory } : {},
	      ...options.frameScheduler ? { frameScheduler: options.frameScheduler } : {},
	      scope: this.scope
	    });
	    const NativeResizeObserver = options.document.defaultView?.ResizeObserver;
	    this.#ownSizeObserver = NativeResizeObserver ? new NativeResizeObserver((entries) => {
	      this.#recordOwnSizeMeasurements(entries);
	    }) : null, this.scope.add(() => {
	      this.#ownSizeObserver?.disconnect(), this.#ownSizeTargets.clear(), this.#ownSizeSamples.clear(), this.#pendingOwnSizePostNumbers.clear();
	    });
	    const createBranchResizeObserver = options.branchResizeObserverFactory ?? (NativeResizeObserver ? (callback) => new NativeResizeObserver(callback) : void 0);
	    this.branchOverlay = new import_branch_overlay.ReaderBranchOverlayController({
	      domOwner: this.domOwner,
	      renderMode: "segmented-css",
	      preserveCollapseAnchor: (root) => {
	        const postNumber = Number(root.dataset.postNumber);
	        if (!Number.isSafeInteger(postNumber) || postNumber <= 0)
	          return this.#pendingBranchCollapseAnchor = null, !1;
	        const scrollRoot = root.closest(".ldp-body"), header = root.querySelector(
	          ":scope > .ldp-post-head"
	        ), viewport = scrollRoot?.getBoundingClientRect(), frozenBottom = scrollRoot?.closest(".ldp-modal")?.querySelector(":scope > .ldp-header")?.getBoundingClientRect().bottom ?? viewport?.top, headerRect = header?.getBoundingClientRect(), rootRect = root.getBoundingClientRect(), visibleTop = Math.max(
	          viewport?.top ?? 0,
	          frozenBottom ?? 0
	        ), parentVisible = !!(viewport && headerRect && headerRect.bottom > visibleTop && headerRect.top < viewport.bottom);
	        return this.#pendingBranchCollapseAnchor = Object.freeze({
	          postNumber,
	          viewportOffset: parentVisible ? Math.max(0, rootRect.top - visibleTop) : 10
	        }), !0;
	      },
	      onLayoutChange: () => {
	        const anchor = this.#pendingBranchCollapseAnchor;
	        this.#pendingBranchCollapseAnchor = null;
	        const postNumber = anchor?.postNumber ?? null, rootPostNumber = postNumber === null ? void 0 : this.replyTreePresentation.rootOf(postNumber) ?? postNumber, offset = postNumber === null ? void 0 : this.#treeViewport.offsetOf(postNumber) ?? this.layout.offsetOf(rootPostNumber);
	        if (postNumber !== null && offset !== void 0) {
	          this.#scroll.writeScrollOffset(offset), this.frame.flushNow();
	          const target = this.domOwner.view(postNumber)?.slots.root;
	          if (target?.isConnected) {
	            const alignCollapsedBranch = (element) => this.#scroll.alignPost(element, {
	              source: "branch-collapse",
	              alignment: "start",
	              viewportOffset: anchor?.viewportOffset ?? 10,
	              highlight: !1
	            });
	            alignCollapsedBranch(target), this.frame.flushNow();
	            const committedTarget = this.domOwner.view(postNumber)?.slots.root;
	            committedTarget?.isConnected && alignCollapsedBranch(committedTarget);
	          }
	        } else
	          this.frame.notifyScroll();
	        this.#scheduleBranchPaint();
	      },
	      onObservedResize: () => this.#scheduleBranchPaint(),
	      ...createBranchResizeObserver ? { createResizeObserver: createBranchResizeObserver } : {},
	      parentScope: this.scope
	    }), this.scope.add(options.scroll.listenScroll(() => {
	      this.frame.notifyScroll();
	    })), this.scope.add(options.scroll.listenUserScrollIntent?.(() => {
	      this.#freezeCanonicalUntilScrollIdle();
	    }) ?? (() => {
	    })), this.scope.add(() => {
	      this.#cancelProjectionHydration(), this.#projectionHydrationFailedPostNumbers.clear(), this.#directReplyPrefetchHandle !== null && this.#directReplyPrefetchScheduler.cancel(
	        this.#directReplyPrefetchHandle
	      ), this.#directReplyPrefetchHandle = null;
	      for (const [postNumber, request] of this.#directReplyPrefetches)
	        request.controller.signal.aborted || request.controller.abort(new DOMException(
	          `树状回复 #${postNumber} 已离开 Topic`,
	          "AbortError"
	        ));
	      this.#directReplyPrefetches.clear(), this.#queuedDirectReplyPrefetches.clear(), this.#syncDirectReplyLoadingIndicators(), this.#directReplyPrefetchedExpectedCounts.clear(), this.#directReplyPrefetchAttemptedExpectedCounts.clear(), this.#directReplyPrefetchCandidatePostNumbers = /* @__PURE__ */ new Set(), this.#directReplyPrefetchOrderedCandidates = Object.freeze([]), this.#directReplyVisiblePostNumbers = /* @__PURE__ */ new Set(), this.#directReplyVisibleKey = "";
	      for (const postNumber of this.#activeContentPostNumbers) {
	        const root = this.domOwner.view(postNumber)?.slots.root;
	        root && this.#deactivateNodeContent(root, postNumber);
	      }
	      for (const postNumber of this.#activeBranchPostNumbers) {
	        const root = this.domOwner.view(postNumber)?.slots.root;
	        root && this.#deactivateBranch(root, postNumber);
	      }
	      this.#activeBranchPostNumbers.clear(), this.#activeContentPostNumbers = /* @__PURE__ */ new Set(), this.#nextContentPostNumbers = /* @__PURE__ */ new Set(), this.#desiredContentPostNumbers = /* @__PURE__ */ new Set(), this.#branchPaintHandle !== null && this.#branchFrames.cancel(this.#branchPaintHandle), this.#branchPaintHandle = null, this.streamView.slots.rootList.classList.remove(
	        "ldp-branch-paint-pending"
	      ), this.visibleRootChanges.clear(), this.windowChanges.clear(), this.#cancelPendingViewportMutation(), this.#ownsPresentationChanges && this.presentationChanges.clear();
	      for (const cleanup of this.#rootObservers.values()) cleanup();
	      this.#rootObservers.clear();
	    }), options.session.changes.subscribe((commit) => {
	      this.#queueSessionCommit(commit);
	    }, this.scope), options.replyTreePreferences?.subscribe((preferences) => {
	      this.replyTreePresentation.update(preferences) && this.refreshRootProjection();
	    }, this.scope);
	  }
	  async initialize() {
	    this.#assertActive();
	    const topic = await this.#session.init();
	    return this.#assertActive(), this.#scroll.writeScrollOffset(0), this.frame.flushNow(), topic;
	  }
	  get preparedPostViewCount() {
	    return this.domOwner.views().length + this.#retainedViews.size;
	  }
	  async loadNext(options = {}) {
	    this.#assertActive();
	    const result = await this.#session.next(options);
	    return this.#assertActive(), this.frame.notifyScroll(), result;
	  }
	  async hydrateUnloadedRange(request, options = {}) {
	    this.#assertActive();
	    let postNumber = (0, import_identifiers.discoursePostNumber)(request.postNumber);
	    if (this.replyTreePresentation.canonicalFrozen) {
	      this.#adoptLatestCanonicalProjection();
	      const window = this.frame.lastCommit?.window;
	      if (request.direction === "around") {
	        if (!(this.visibleDataGapPostNumber() === postNumber) && window?.unloadedGapTargetPostNumber === void 0) return 0;
	      } else if (request.direction === "before") {
	        if (window?.hasUnloadedGapBefore !== !0) return 0;
	        postNumber = window.unloadedGapBeforeAnchorPostNumber ?? window.segmentStartPostNumber ?? postNumber;
	      } else {
	        if (window?.hasUnloadedGapAfter !== !0) return 0;
	        postNumber = window.unloadedGapAfterAnchorPostNumber ?? window.segmentEndPostNumber ?? postNumber;
	      }
	    }
	    if (request.direction === "around" && this.#session.postByNumber(postNumber) !== void 0)
	      return this.refreshRootProjection(), 0;
	    let posts;
	    return request.direction === "before" ? posts = this.#session.loadBeforePost ? await this.#session.loadBeforePost(postNumber, options) : Object.freeze([]) : request.direction === "after" ? posts = this.#session.loadAfterPost ? await this.#session.loadAfterPost(postNumber, options) : Object.freeze([]) : posts = this.#session.loadAroundPost ? await this.#session.loadAroundPost(postNumber, options) : Object.freeze([]), this.#assertActive(), this.#adoptLatestCanonicalProjection() || this.frame.notifyScroll(), posts.length;
	  }
	  async prefetchAhead(batchCount) {
	    this.#assertActive(), this.#session.prefetchAhead && (await this.#session.prefetchAhead(batchCount, {
	      background: !0,
	      maxAttempts: 1
	    }), this.#assertActive(), this.frame.notifyScroll());
	  }
	  setFlowStatus(state) {
	    this.#assertActive(), this.streamView.setFlowState(Object.freeze({
	      ...state,
	      empty: state.done && this.replyTreePresentation.roots().length === 0
	    }));
	  }
	  notifyScroll() {
	    this.#assertActive(), this.frame.notifyScroll();
	  }
	  /**
	   * cooked、图片、Onebox、公式等迟到内容只报告“几何已经变化”。
	   *
	   * 根高度由 VirtualStreamFrameController 的唯一 ResizeObserver 读取并提交;
	   * 这里不得再伪造一次 scroll 帧,否则同一次资源 load 会同时走手工通知与
	   * ResizeObserver 两条路径。回复线只需在下一绘制帧读取最终锚点。
	   */
	  notifyContentLayoutChanged() {
	    this.#assertActive(), this.#scheduleBranchPaint();
	  }
	  flushNow() {
	    this.#assertActive(), this.frame.flushNow();
	  }
	  refreshRootProjection(resetScroll = !1) {
	    this.#assertActive(), this.#rootProjection.syncRoots(), resetScroll && this.#scroll.writeScrollOffset(0), this.frame.flushNow(), this.#syncProjectionFeatures();
	  }
	  revealNextReplyLevel(postNumberValue) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostNumber)(postNumberValue);
	    return this.replyTreePresentation.revealNextLevel(postNumber) ? (this.refreshRootProjection(), !0) : !1;
	  }
	  readWindowInput() {
	    return this.#assertActive(), this.#readVirtualWindowInput();
	  }
	  #readVirtualWindowInput() {
	    const input = this.#scroll.readWindowInput(), preservePostNumber = input.preservePostNumber, preserveRootPostNumber = preservePostNumber === void 0 ? void 0 : this.replyTreePresentation.rootOf(preservePostNumber) ?? preservePostNumber;
	    return Object.freeze({
	      ...input,
	      materializationStepScreens: MATERIALIZATION_STEP_SCREENS,
	      ...preserveRootPostNumber === void 0 ? {} : { preserveRootPostNumber }
	    });
	  }
	  hasVisibleDataGap() {
	    return this.#assertActive(), this.visibleDataGapPostNumber() !== void 0;
	  }
	  visibleDataGapPostNumber() {
	    this.#assertActive();
	    for (const visiblePostNumber of this.#treeViewport.visiblePostNumbers) {
	      let postNumber = visiblePostNumber, rootmostMissingPostNumber;
	      const visited = /* @__PURE__ */ new Set();
	      for (; postNumber != null && !visited.has(postNumber); )
	        visited.add(postNumber), this.#session.postByNumber(postNumber) || (rootmostMissingPostNumber = postNumber), postNumber = this.replyTreePresentation.parentOf(postNumber);
	      if (rootmostMissingPostNumber !== void 0)
	        return rootmostMissingPostNumber;
	    }
	  }
	  lastUserScrollAt() {
	    return this.#assertActive(), this.#scrollLifecycle.lastUserScrollAt();
	  }
	  lastUserScrollDirection() {
	    this.#assertActive();
	    const direction = this.#scroll.lastUserScrollDirection?.() ?? 0;
	    return direction === -1 || direction === 1 ? direction : 0;
	  }
	  listenUserScrollIntent(listener) {
	    return this.#assertActive(), this.#scroll.listenUserScrollIntent?.(listener) ?? (() => {
	    });
	  }
	  listenDirectUserScrollIntent(listener) {
	    return this.#assertActive(), this.#scroll.listenDirectUserScrollIntent?.(listener) ?? (() => {
	    });
	  }
	  captureViewportAnchor() {
	    this.#assertActive();
	    const physicalAnchor = this.#scroll.readVisibleViewportAnchor?.(
	      this.#visiblePostElements()
	    ), input = this.#scroll.readWindowInput(), scrollRange = this.#scrollRange(input), scrollRatio = scrollRange > 0 ? Math.min(1, Math.max(0, input.scrollOffset / scrollRange)) : 0;
	    if (physicalAnchor) {
	      const postNumber = (0, import_identifiers.discoursePostReference)({
	        post_number: physicalAnchor.postNumber
	      }).postNumber, rootPostNumber = this.domOwner.topology.rootOf(postNumber), postLayoutOffset = this.#treeViewport.offsetOf(postNumber) ?? (rootPostNumber === void 0 ? void 0 : this.layout.offsetOf(rootPostNumber));
	      if (postLayoutOffset !== void 0)
	        return Object.freeze({
	          postNumber,
	          postOffset: input.scrollOffset - postLayoutOffset,
	          scrollTop: input.scrollOffset,
	          scrollRange,
	          scrollRatio
	        });
	    }
	    const visibleRootPostNumber = this.layout.window({
	      ...input,
	      overscanBeforeScreens: 0,
	      overscanAfterScreens: 0
	    }).visiblePostNumbers[0];
	    if (visibleRootPostNumber === void 0) return null;
	    const rootOffset = this.layout.offsetOf(visibleRootPostNumber);
	    return rootOffset === void 0 ? null : Object.freeze({
	      postNumber: visibleRootPostNumber,
	      postOffset: input.scrollOffset - rootOffset,
	      scrollTop: input.scrollOffset,
	      scrollRange,
	      scrollRatio
	    });
	  }
	  #scrollRange(input) {
	    const physicalRange = Number(this.#scroll.readScrollRange?.());
	    if (Number.isFinite(physicalRange) && physicalRange > 0)
	      return physicalRange;
	    const virtualRange = Math.max(
	      0,
	      this.layout.window(input).totalSize - input.viewportSize
	    );
	    return virtualRange > 0 ? virtualRange : Math.max(0, Number.isFinite(physicalRange) ? physicalRange : 0);
	  }
	  #connectedPostElements() {
	    return this.domOwner.views().map((view) => view.slots.root).filter(
	      (root) => root.isConnected && this.streamView.slots.root.contains(root)
	    );
	  }
	  #visiblePostElements() {
	    const visible = this.#treeViewport.visiblePostNumbers.map((postNumber) => this.domOwner.view(postNumber)?.slots.root).filter(
	      (root) => !!root && root.isConnected && this.streamView.slots.root.contains(root)
	    );
	    return visible.length ? Object.freeze(visible) : this.#connectedPostElements();
	  }
	  restoreViewportAnchor(anchor) {
	    this.#assertActive();
	    const scrollRatio = Number(anchor.scrollRatio);
	    if (Number.isFinite(scrollRatio) && scrollRatio >= 0)
	      return this.#writeVirtualOffset(() => {
	        const input = this.#scroll.readWindowInput();
	        return this.#scrollRange(input) * Math.min(1, scrollRatio);
	      });
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: anchor.postNumber
	    }).postNumber, rootPostNumber = this.domOwner.topology.rootOf(postNumber);
	    if (rootPostNumber === void 0) return !1;
	    const postOffset = Number(anchor.postOffset);
	    return this.#writeVirtualOffset(() => {
	      const postLayoutOffset = this.#treeViewport.offsetOf(postNumber) ?? this.layout.offsetOf(rootPostNumber);
	      return postLayoutOffset === void 0 ? void 0 : postLayoutOffset + (Number.isFinite(postOffset) ? postOffset : 0);
	    });
	  }
	  /** 只闪烁已挂载楼层,不改变当前视口几何。 */
	  highlightPost(rawPostNumber) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber, element = this.domOwner.view(postNumber)?.slots.root;
	    return !element?.isConnected || element.classList.contains("ldp-virtual-ancestor-shell") || element.classList.contains("ldp-post-projection-pending") || !this.#scroll.highlightPost ? !1 : (this.#scroll.highlightPost(element), !0);
	  }
	  /** 当前 canonical 楼层被主信息流投影停放时,交由完整讨论 surface 揭示。 */
	  isPostHidden(rawPostNumber) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber;
	    return this.replyTreePresentation.canonical.has(postNumber) && this.replyTreePresentation.rootOf(postNumber) === void 0;
	  }
	  /** 目的性导航取代当前滚动手势,并用最新回复关系判定目标属于正文还是隐藏子树。 */
	  prepareRevealPost(rawPostNumber) {
	    this.#assertActive(), (0, import_identifiers.discoursePostReference)({ post_number: rawPostNumber }), this.#adoptLatestCanonicalProjection();
	  }
	  revealPost(rawPostNumber, options) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber;
	    options.revealAsFloor === !0 && this.replyTreePresentation.revealAsFloor(postNumber) && (this.#rootProjection.syncRoots(), this.#syncProjectionFeatures());
	    let rootPostNumber = this.domOwner.topology.rootOf(postNumber);
	    if (rootPostNumber === void 0 && options.degradedRootPostNumber !== void 0 && (this.replyTreePresentation.revealDegradedBranch(
	      options.degradedRootPostNumber,
	      postNumber
	    ), this.#rootProjection.syncRoots(), this.#syncProjectionFeatures(), rootPostNumber = this.domOwner.topology.rootOf(postNumber)), rootPostNumber === void 0 && this.replyTreePresentation.revealAsFloor(postNumber) && (this.#rootProjection.syncRoots(), rootPostNumber = postNumber, this.#syncProjectionFeatures()), rootPostNumber === void 0) return null;
	    const currentRoot = this.domOwner.view(postNumber)?.slots.root, wasMaterialized = currentRoot?.isConnected === !0 && !currentRoot.classList.contains("ldp-virtual-ancestor-shell") && !currentRoot.classList.contains("ldp-post-projection-pending");
	    if (!wasMaterialized && !this.#writeVirtualOffset(
	      () => this.#treeViewport.offsetOf(postNumber) ?? this.layout.offsetOf(rootPostNumber)
	    ) || this.domOwner.view(postNumber)?.slots.root?.classList.contains("ldp-post-projection-pending") && !this.#materializeProjection(postNumber, !1)) return null;
	    const element = this.domOwner.view(postNumber)?.slots.root;
	    return !element?.isConnected || element.classList.contains("ldp-virtual-ancestor-shell") || element.classList.contains("ldp-post-projection-pending") ? null : (this.#scroll.alignPost(element, options), Object.freeze({
	      postNumber,
	      rootPostNumber,
	      element,
	      mounted: !wasMaterialized
	    }));
	  }
	  #writeVirtualOffset(readOffset) {
	    let written = !1;
	    const commit = () => {
	      this.frame.flushNow();
	      const offset = readOffset();
	      offset !== void 0 && (this.#scroll.writeScrollOffset(offset), this.frame.flushNow(), written = !0);
	    };
	    return this.#scroll.withProgrammaticScrollTransaction ? this.#scroll.withProgrammaticScrollTransaction(commit) : commit(), written;
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #queueSessionCommit(commit) {
	    const postStreamRevision = this.#session.postStreamRevision ?? 0, postStreamGeometryChanged = postStreamRevision !== this.#lastPostStreamRevision;
	    this.#lastPostStreamRevision = postStreamRevision, this.#beginViewportMutation(commit, postStreamGeometryChanged);
	    try {
	      this.#rootProjection.syncRoots(), this.#applySessionCommit(commit), this.#emitPresentationCommit(commit);
	    } catch (error) {
	      throw this.#cancelPendingViewportMutation(), error;
	    }
	  }
	  #beginViewportMutation(commit, postStreamGeometryChanged = !1) {
	    this.#commitMayChangeConnectedGeometry(
	      commit,
	      postStreamGeometryChanged
	    ) && this.#beginConnectedViewportMutation();
	  }
	  #commitMayChangeConnectedGeometry(commit, postStreamGeometryChanged) {
	    if (postStreamGeometryChanged && this.#connectedPostElements().length > 0) return !0;
	    if (commit.topicChanged || commit.streamChanged)
	      return this.#connectedPostElements().length > 0;
	    for (const postNumber of [
	      ...commit.changedPostNumbers,
	      ...commit.removedPostNumbers ?? []
	    ]) {
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      if (root?.isConnected && this.streamView.slots.root.contains(root)) return !0;
	      const rootPostNumber = this.replyTreePresentation.rootOf(postNumber);
	      if (rootPostNumber === void 0 || rootPostNumber === postNumber)
	        continue;
	      const connectedRoot = this.domOwner.view(rootPostNumber)?.slots.root;
	      if (connectedRoot?.isConnected && this.streamView.slots.root.contains(connectedRoot)) return !0;
	    }
	    return !1;
	  }
	  #beginConnectedViewportMutation() {
	    if (this.#pendingViewportMutation || !this.#scroll.beginViewportMutation) return null;
	    const elements = this.#connectedPostElements();
	    if (!elements.length) return null;
	    const mutation = this.#scroll.beginViewportMutation(elements);
	    return this.#pendingViewportMutation = mutation, mutation;
	  }
	  #freezeCanonicalUntilScrollIdle() {
	    if (!this.replyTreePresentation.freezeCanonical()) return;
	    const epoch = ++this.#canonicalFreezeEpoch;
	    this.#scrollLifecycle.waitForIdle().then(() => {
	      if (!(this.#destroyed || epoch !== this.#canonicalFreezeEpoch))
	        try {
	          this.#adoptLatestCanonicalProjection();
	        } catch (error) {
	          this.#onError(error);
	        }
	    }).catch(this.#onError);
	  }
	  #adoptLatestCanonicalProjection() {
	    if (!this.replyTreePresentation.canonicalFrozen) return !1;
	    if (this.#canonicalFreezeEpoch += 1, !this.replyTreePresentation.thawCanonical()) return !0;
	    const mutation = this.#beginConnectedViewportMutation();
	    try {
	      this.refreshRootProjection(), mutation && this.#pendingViewportMutation === mutation && this.#restorePendingViewportMutation();
	    } catch (error) {
	      throw mutation && this.#pendingViewportMutation === mutation && this.#cancelPendingViewportMutation(), error;
	    }
	    return !0;
	  }
	  #restorePendingViewportMutation() {
	    const mutation = this.#pendingViewportMutation;
	    mutation && (this.#pendingViewportMutation = null, mutation.restore());
	  }
	  #cancelPendingViewportMutation() {
	    const mutation = this.#pendingViewportMutation;
	    mutation && (this.#pendingViewportMutation = null, mutation.cancel());
	  }
	  #emitPresentationCommit(commit) {
	    for (const error of this.presentationChanges.emit(commit))
	      this.#onError(error);
	  }
	  #applySessionCommit(commit, notify = !0) {
	    this.#directReplyPrefetchCandidateKey = "";
	    for (const postNumber of commit.removedPostNumbers ?? []) {
	      this.#projectionHydrationFailedPostNumbers.delete(postNumber), this.#directReplyPrefetchedExpectedCounts.delete(postNumber), this.#directReplyPrefetchAttemptedExpectedCounts.delete(postNumber);
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      this.#activeContentPostNumbers.has(postNumber) && (root && this.#deactivateNodeContent(root, postNumber), this.#activeContentPostNumbers = new Set(
	        [...this.#activeContentPostNumbers].filter(
	          (candidate) => candidate !== postNumber
	        )
	      )), root && this.#rootObservers.has(postNumber) && this.#deactivateBranch(root, postNumber), this.#rootObservers.get(postNumber)?.(), this.#rootObservers.delete(postNumber), this.domOwner.unregister(postNumber, !0, !1), this.#retainedViews.get(postNumber)?.destroy(), this.#retainedViews.delete(postNumber), this.#topicDirtyRetainedPostNumbers.delete(postNumber);
	    }
	    const refreshPostNumbers = new Set(commit.changedPostNumbers);
	    for (const postNumber of refreshPostNumbers)
	      this.#projectionHydrationFailedPostNumbers.delete(postNumber);
	    if (commit.topicChanged) {
	      for (const postNumber of this.#mountedPostNumbers)
	        refreshPostNumbers.add(postNumber);
	      for (const postNumber of this.#retainedViews.keys())
	        this.#topicDirtyRetainedPostNumbers.add(postNumber);
	    }
	    const posts = [...refreshPostNumbers].map((postNumber) => this.#session.postByNumber(postNumber)).filter((post) => post !== void 0);
	    this.#refreshExistingPosts(posts);
	    for (const postNumber of refreshPostNumbers)
	      this.#directReplyPrefetchCandidatePostNumbers.has(postNumber) && this.#prefetchDirectReplies(postNumber);
	    notify && this.frame.notifyScroll();
	  }
	  #refreshExistingPosts(posts) {
	    for (const post of posts) {
	      let postNumber;
	      try {
	        postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber;
	      } catch (error) {
	        this.#onError(error);
	        continue;
	      }
	      const existing = this.domOwner.view(postNumber) ?? this.#retainedViews.get(postNumber);
	      if (existing && !existing.slots.root.classList.contains(
	        "ldp-post-projection-pending"
	      ))
	        try {
	          this.postProjector.render(post, existing), this.#topicDirtyRetainedPostNumbers.delete(postNumber);
	        } catch (error) {
	          this.#onError(error);
	        }
	    }
	  }
	  #prepareRootViews(input, window) {
	    this.#updateRetainedViewLimit(input.maxMountedPostCount);
	    let plan = this.#treeViewport.plan(window, input);
	    if (!this.#ownSizeObserver)
	      for (let pass = 0; pass < 2; pass += 1) {
	        let measured = !1;
	        for (const postNumber of this.#activeContentPostNumbers)
	          plan.contentPostNumbers.has(postNumber) || (measured = this.#measureOwnPostSize(postNumber) || measured);
	        if (!measured) break;
	        plan = this.#treeViewport.plan(window, input);
	      }
	    this.#rootVirtualInsets = plan.rootVirtualInsets ?? /* @__PURE__ */ new Map(), this.#mountedPostNumbers = plan.mountedPostNumbers, this.#desiredContentPostNumbers = new Set(plan.contentPostNumbers);
	    const visiblePostNumbers = new Set(
	      this.#treeViewport.visiblePostNumbers
	    );
	    let eagerProjectionBudget = this.#scrollLifecycle.isIdle(
	      PROJECTION_HYDRATION_MIN_IDLE_MS
	    ) ? plan.contentPostNumbers.size : 0;
	    for (const postNumber of plan.mountedPostNumbers) {
	      if (this.domOwner.view(postNumber)) continue;
	      const retained = this.#retainedViews.get(postNumber);
	      if (retained) {
	        if (this.#retainedViews.delete(postNumber), this.domOwner.register(retained, !1), !retained.slots.root.classList.contains(
	          "ldp-post-projection-pending"
	        ) && this.#topicDirtyRetainedPostNumbers.delete(postNumber)) {
	          const post2 = this.#session.postByNumber(postNumber);
	          if (post2)
	            try {
	              this.postProjector.render(post2, retained);
	            } catch (error) {
	              this.#onError(error);
	            }
	        }
	        continue;
	      }
	      const post = this.#session.postByNumber(postNumber);
	      if (post)
	        try {
	          const renderImmediately = eagerProjectionBudget > 0 && plan.contentPostNumbers.has(postNumber), created = renderImmediately ? this.postProjector.create(
	            post,
	            this.scope,
	            postNumber
	          ) : this.postProjector.createShell(
	            post,
	            this.scope,
	            postNumber
	          );
	          renderImmediately ? eagerProjectionBudget -= 1 : this.#markProjectionPending(
	            created,
	            plan.ownSizes.get(postNumber)
	          ), this.domOwner.register(created, !1);
	        } catch (error) {
	          this.#onError(error);
	        }
	    }
	    return this.#nextContentPostNumbers = new Set(
	      [...plan.contentPostNumbers].filter((postNumber) => {
	        const root = this.domOwner.view(postNumber)?.slots.root;
	        return !!root && !root.classList.contains(
	          "ldp-post-projection-pending"
	        );
	      })
	    ), this.#setProjectionHydrationCandidates(
	      [
	        ...visiblePostNumbers,
	        ...plan.contentPostNumbers
	      ]
	    ), plan;
	  }
	  #markProjectionPending(view, ownSize) {
	    const root = view.slots.root;
	    root.classList.add("ldp-post-projection-pending"), root.setAttribute("aria-busy", "true"), ownSize !== void 0 && root.style.setProperty(
	      "--ldp-virtual-own-size",
	      `${Math.max(0, ownSize)}px`
	    );
	  }
	  #setProjectionHydrationCandidates(postNumbers) {
	    const unique = /* @__PURE__ */ new Set();
	    for (const postNumber of postNumbers)
	      unique.has(postNumber) || !this.#mountedPostNumbers.has(postNumber) || this.#projectionHydrationFailedPostNumbers.has(postNumber) || !this.domOwner.view(postNumber)?.slots.root?.classList.contains("ldp-post-projection-pending") || unique.add(postNumber);
	    if (this.#projectionHydrationPostNumbers = Object.freeze([...unique]), !this.#projectionHydrationPostNumbers.length) {
	      this.#projectionHydrationHandle !== null && this.#projectionHydrationScheduler.cancel(
	        this.#projectionHydrationHandle
	      ), this.#projectionHydrationHandle = null;
	      return;
	    }
	    this.#scheduleProjectionHydration();
	  }
	  #scheduleProjectionHydration(delayMs) {
	    if (this.scope.destroyed || this.#projectionHydrationHandle !== null || !this.#projectionHydrationPostNumbers.length) return;
	    const delay = delayMs ?? Math.max(
	      PROJECTION_HYDRATION_BATCH_DELAY_MS,
	      this.#scrollLifecycle.remainingIdleMs(
	        PROJECTION_HYDRATION_MIN_IDLE_MS
	      )
	    );
	    this.#projectionHydrationHandle = this.#projectionHydrationScheduler.schedule(() => {
	      if (this.#projectionHydrationHandle = null, this.scope.destroyed) return;
	      const remainingIdleMs = this.#scrollLifecycle.remainingIdleMs(
	        PROJECTION_HYDRATION_MIN_IDLE_MS
	      );
	      if (remainingIdleMs > 0) {
	        this.#scheduleProjectionHydration(remainingIdleMs);
	        return;
	      }
	      this.#hydrateProjectionBatch();
	    }, Math.max(0, delay));
	  }
	  #hydrateProjectionBatch() {
	    const remaining = [];
	    let hydrated = 0;
	    for (const postNumber of this.#projectionHydrationPostNumbers) {
	      if (hydrated < PROJECTION_HYDRATION_BATCH_SIZE && this.#materializeProjection(postNumber)) {
	        hydrated += 1;
	        continue;
	      }
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      this.#mountedPostNumbers.has(postNumber) && !this.#projectionHydrationFailedPostNumbers.has(postNumber) && root?.classList.contains("ldp-post-projection-pending") && remaining.push(postNumber);
	    }
	    this.#projectionHydrationPostNumbers = Object.freeze(remaining), remaining.length && this.#scheduleProjectionHydration(
	      PROJECTION_HYDRATION_BATCH_DELAY_MS
	    );
	  }
	  #materializeProjection(postNumber, notify = !0) {
	    if (!this.#mountedPostNumbers.has(postNumber) || this.#projectionHydrationFailedPostNumbers.has(postNumber)) return !1;
	    const post = this.#session.postByNumber(postNumber), view = this.domOwner.view(postNumber), root = view?.slots.root;
	    if (!post || !view || !root?.classList.contains("ldp-post-projection-pending")) return !1;
	    const mutation = notify && root.isConnected ? this.#beginConnectedViewportMutation() : null;
	    try {
	      this.postProjector.render(post, view);
	    } catch (error) {
	      return this.#projectionHydrationFailedPostNumbers.add(postNumber), mutation && this.#pendingViewportMutation === mutation && this.#cancelPendingViewportMutation(), this.#onError(error), !1;
	    }
	    return this.#topicDirtyRetainedPostNumbers.delete(postNumber), root.classList.remove("ldp-post-projection-pending"), root.removeAttribute("aria-busy"), root.isConnected && this.#rootObservers.has(postNumber) && this.#activateBranch(root, postNumber), root.isConnected && this.#desiredContentPostNumbers.has(postNumber) && !this.#activeContentPostNumbers.has(postNumber) && (this.#activeContentPostNumbers = /* @__PURE__ */ new Set([
	      ...this.#activeContentPostNumbers,
	      postNumber
	    ]), this.#activateNodeContent(root, postNumber)), this.#syncProjectionFeatures(), notify && this.frame.notifyScroll(), this.#scheduleBranchPaint(), !0;
	  }
	  #cancelProjectionHydration() {
	    this.#projectionHydrationHandle !== null && this.#projectionHydrationScheduler.cancel(
	      this.#projectionHydrationHandle
	    ), this.#projectionHydrationHandle = null, this.#projectionHydrationPostNumbers = Object.freeze([]);
	  }
	  #measureOwnPostSize(postNumber) {
	    const view = this.domOwner.view(postNumber), root = view?.slots.root;
	    if (!view || !root?.isConnected || root.classList.contains("ldp-virtual-ancestor-shell")) return !1;
	    const rootSize = root.getBoundingClientRect().height, replyTreeSize = view.slots.replyTree.getBoundingClientRect().height, ownSize = rootSize - replyTreeSize;
	    return this.#treeViewport.measureOwnSize(postNumber, ownSize);
	  }
	  #releaseParkedViews(parkedPostNumbers) {
	    for (const postNumber of parkedPostNumbers) {
	      if (this.#mountedPostNumbers.has(postNumber)) continue;
	      const view = this.domOwner.unregister(postNumber, !1, !1);
	      view && this.#retainView(view);
	    }
	  }
	  #updateRetainedViewLimit(maxMountedPostCount) {
	    this.#retainedViewLimit = maxMountedPostCount === void 0 ? 0 : Math.max(0, Math.floor(maxMountedPostCount / 4)), this.#trimRetainedViews();
	  }
	  #retainView(view) {
	    this.#retainedViews.delete(view.postNumber), this.#retainedViews.set(view.postNumber, view), this.#trimRetainedViews();
	  }
	  #trimRetainedViews() {
	    for (; this.#retainedViews.size > this.#retainedViewLimit; ) {
	      const postNumber = this.#retainedViews.keys().next().value;
	      if (postNumber === void 0) break;
	      const view = this.#retainedViews.get(postNumber);
	      this.#retainedViews.delete(postNumber), this.#topicDirtyRetainedPostNumbers.delete(postNumber), this.#projectionHydrationFailedPostNumbers.delete(postNumber), view?.destroy();
	    }
	  }
	  #syncContentFeatures() {
	    const previousPostNumbers = this.#activeContentPostNumbers, nextPostNumbers = new Set(this.#nextContentPostNumbers);
	    for (const postNumber of previousPostNumbers) {
	      if (nextPostNumbers.has(postNumber)) continue;
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      root && this.#deactivateNodeContent(root, postNumber);
	    }
	    this.#activeContentPostNumbers = nextPostNumbers;
	    for (const postNumber of nextPostNumbers) {
	      if (previousPostNumbers.has(postNumber)) continue;
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      root && this.#activateNodeContent(root, postNumber);
	    }
	  }
	  #activateNodeContent(root, postNumber) {
	    this.postProjector.attach(root, postNumber, "node");
	    const view = this.domOwner.view(postNumber);
	    view && this.#observeOwnSize(view);
	  }
	  #deactivateNodeContent(root, postNumber) {
	    const view = this.domOwner.view(postNumber);
	    view && this.#unobserveOwnSize(view), this.postProjector.detach(root, postNumber, "node");
	  }
	  #observeOwnSize(view) {
	    const observer = this.#ownSizeObserver;
	    if (!observer) return;
	    const postNumber = view.postNumber;
	    for (const [target, kind] of [
	      [view.slots.root, "root"],
	      [view.slots.replyTree, "reply-tree"]
	    ])
	      this.#ownSizeTargets.set(target, Object.freeze({ postNumber, kind })), observer.observe(target);
	  }
	  #unobserveOwnSize(view) {
	    const observer = this.#ownSizeObserver;
	    if (observer) {
	      for (const target of [view.slots.root, view.slots.replyTree])
	        observer.unobserve(target), this.#ownSizeTargets.delete(target);
	      this.#ownSizeSamples.delete(view.postNumber), this.#pendingOwnSizePostNumbers.delete(view.postNumber);
	    }
	  }
	  #recordOwnSizeMeasurements(entries) {
	    const touched = /* @__PURE__ */ new Set();
	    for (const entry of entries) {
	      const target = this.#ownSizeTargets.get(entry.target);
	      if (!target) continue;
	      const borderBox = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize, blockSize = Math.round(
	        borderBox?.blockSize ?? entry.contentRect.height
	      );
	      if (!Number.isFinite(blockSize) || blockSize < 0) continue;
	      const sample = this.#ownSizeSamples.get(target.postNumber) ?? {};
	      target.kind === "root" ? sample.root = blockSize : sample.replyTree = blockSize, this.#ownSizeSamples.set(target.postNumber, sample), touched.add(target.postNumber);
	    }
	    for (const postNumber of touched)
	      this.#pendingOwnSizePostNumbers.add(postNumber);
	    this.#commitPendingOwnSizeMeasurements();
	  }
	  #commitPendingOwnSizeMeasurements() {
	    if (!this.#pendingOwnSizePostNumbers.size) return;
	    const pending = [...this.#pendingOwnSizePostNumbers];
	    this.#pendingOwnSizePostNumbers.clear();
	    let changed = !1;
	    for (const postNumber of pending) {
	      const view = this.domOwner.view(postNumber), sample = this.#ownSizeSamples.get(postNumber);
	      !view?.slots.root.isConnected || view.slots.root.classList.contains("ldp-virtual-ancestor-shell") || sample?.root === void 0 || sample.replyTree === void 0 || (changed = this.#treeViewport.measureOwnSize(
	        postNumber,
	        Math.max(1, sample.root - sample.replyTree)
	      ) || changed);
	    }
	    changed && this.frame.notifyScroll();
	  }
	  #syncDirectReplyPrefetchCandidates() {
	    const input = this.#scroll.readWindowInput(), rawScreens = Number(this.#readDirectReplyPrefetchScreens()), prefetchScreens = Number.isFinite(rawScreens) ? Math.min(3, Math.max(0, rawScreens)) : 0, beforeScreens = Math.max(
	      prefetchScreens,
	      Number(input.overscanBeforeScreens) || 0
	    ), afterScreens = Math.max(
	      prefetchScreens,
	      Number(input.overscanAfterScreens) || 0
	    ), scrollBucketSize = Math.max(1, input.viewportSize / 2), candidateKey = `${this.replyTreePresentation.revision}|${Math.floor(input.scrollOffset / scrollBucketSize)}|${Math.round(input.viewportSize)}|${beforeScreens}|${afterScreens}`, candidatesChanged = candidateKey !== this.#directReplyPrefetchCandidateKey;
	    if (this.#directReplyPrefetchCandidateKey = candidateKey, candidatesChanged) {
	      const prefetchInput = Object.freeze({
	        scrollOffset: input.scrollOffset,
	        viewportSize: input.viewportSize,
	        overscanBeforeScreens: beforeScreens,
	        overscanAfterScreens: afterScreens
	      }), rootWindow = this.layout.window(prefetchInput), plan = this.#treeViewport.plan(rootWindow, prefetchInput), candidateOffsets = new Map(
	        [...plan.contentPostNumbers].map((postNumber) => [
	          postNumber,
	          this.#treeViewport.offsetOf(postNumber) ?? 0
	        ])
	      );
	      this.#directReplyPrefetchOrderedCandidates = Object.freeze(
	        [...plan.contentPostNumbers].sort((left, right) => (candidateOffsets.get(left) ?? 0) - (candidateOffsets.get(right) ?? 0) || left - right)
	      ), this.#directReplyPrefetchCandidatePostNumbers = new Set(
	        this.#directReplyPrefetchOrderedCandidates
	      );
	    }
	    const visiblePostNumbers = new Set(
	      this.#treeViewport.visiblePostNumbers.filter(
	        (postNumber) => this.#directReplyPrefetchCandidatePostNumbers.has(postNumber)
	      )
	    ), visibleKey = [...visiblePostNumbers].join(","), visibleChanged = visibleKey !== this.#directReplyVisibleKey;
	    visibleChanged && (this.#directReplyVisibleKey = visibleKey), (candidatesChanged || visibleChanged) && this.#abortDirectReplyRequestsOutside(
	      this.#directReplyPrefetchCandidatePostNumbers,
	      visiblePostNumbers
	    ), this.#directReplyVisiblePostNumbers = visiblePostNumbers;
	    const orderedCandidates = [
	      ...this.#directReplyPrefetchOrderedCandidates.filter((postNumber) => visiblePostNumbers.has(postNumber)),
	      ...this.#directReplyPrefetchOrderedCandidates.filter((postNumber) => !visiblePostNumbers.has(postNumber))
	    ], stillQueued = orderedCandidates.filter(
	      (postNumber) => this.#queuedDirectReplyPrefetches.has(postNumber)
	    );
	    this.#queuedDirectReplyPrefetches.clear();
	    for (const postNumber of stillQueued)
	      this.#queuedDirectReplyPrefetches.add(postNumber);
	    if (candidatesChanged || visiblePostNumbers.size > 0)
	      for (const postNumber of orderedCandidates)
	        this.#prefetchDirectReplies(postNumber, !1);
	    this.#queuedDirectReplyPrefetches.size > 0 ? this.#scheduleDirectReplyPrefetchFlush() : this.#directReplyPrefetchHandle !== null && (this.#directReplyPrefetchScheduler.cancel(
	      this.#directReplyPrefetchHandle
	    ), this.#directReplyPrefetchHandle = null), this.#syncDirectReplyLoadingIndicators();
	  }
	  #prefetchDirectReplies(postNumber, schedule = !0) {
	    this.#directReplyPrefetches.has(postNumber) || this.#queuedDirectReplyPrefetches.has(postNumber) || this.scope.destroyed || !this.#directReplyPrefetchRequired(postNumber) || (this.#queuedDirectReplyPrefetches.add(postNumber), this.#syncDirectReplyLoadingIndicators(), schedule && this.#scheduleDirectReplyPrefetchFlush());
	  }
	  #directReplyPrefetchRequired(postNumber) {
	    if (!this.#session.loadDirectReplies) return !1;
	    const expectedCount = Number(
	      this.#session.postByNumber(postNumber)?.reply_count ?? 0
	    );
	    return Number.isSafeInteger(expectedCount) && expectedCount > 0 && Math.max(
	      this.#directReplyPrefetchedExpectedCounts.get(postNumber) ?? 0,
	      this.#directReplyPrefetchAttemptedExpectedCounts.get(postNumber) ?? 0
	    ) < expectedCount;
	  }
	  #scheduleDirectReplyPrefetchFlush(minimumDelayMs = 0) {
	    this.scope.destroyed || this.#queuedDirectReplyPrefetches.size === 0 || (this.#directReplyPrefetchHandle !== null && this.#directReplyPrefetchScheduler.cancel(
	      this.#directReplyPrefetchHandle
	    ), this.#directReplyPrefetchHandle = this.#directReplyPrefetchScheduler.schedule(() => {
	      this.#directReplyPrefetchHandle = null, this.#flushDirectReplyPrefetch();
	    }, Math.max(0, minimumDelayMs)));
	  }
	  #flushDirectReplyPrefetch() {
	    if (this.scope.destroyed) return;
	    for (const candidate of this.#queuedDirectReplyPrefetches)
	      this.#directReplyPrefetchCandidatePostNumbers.has(candidate) || this.#queuedDirectReplyPrefetches.delete(candidate);
	    let visiblePostNumber = [...this.#queuedDirectReplyPrefetches].find(
	      (candidate) => this.#directReplyVisiblePostNumbers.has(candidate)
	    );
	    for (; visiblePostNumber !== void 0 && this.#directReplyPrefetches.size < DIRECT_REPLY_VISIBLE_PIPELINE_DEPTH; )
	      this.#startDirectReplyRequest(visiblePostNumber, "visible"), visiblePostNumber = [...this.#queuedDirectReplyPrefetches].find(
	        (candidate) => this.#directReplyVisiblePostNumbers.has(candidate)
	      );
	    if (visiblePostNumber !== void 0 || [...this.#directReplyPrefetches.values()].some(
	      (request) => request.lane === "visible"
	    )) {
	      this.#syncDirectReplyLoadingIndicators();
	      return;
	    }
	    const configuredConcurrency = Number(
	      this.#readDirectReplyPrefetchConcurrency()
	    ), nearbyDepth = Math.min(
	      DIRECT_REPLY_NEARBY_PIPELINE_DEPTH,
	      Math.max(
	        1,
	        Number.isFinite(configuredConcurrency) ? Math.round(configuredConcurrency) : 1
	      )
	    );
	    let nearbyCount = [...this.#directReplyPrefetches.values()].filter(
	      (request) => request.lane === "nearby"
	    ).length, postNumber = [...this.#queuedDirectReplyPrefetches].find(
	      (candidate) => this.#directReplyPrefetchCandidatePostNumbers.has(candidate)
	    );
	    for (; postNumber !== void 0 && nearbyCount < nearbyDepth; )
	      this.#startDirectReplyRequest(postNumber, "nearby"), nearbyCount += 1, postNumber = [...this.#queuedDirectReplyPrefetches].find(
	        (candidate) => this.#directReplyPrefetchCandidatePostNumbers.has(candidate)
	      );
	    nearbyCount === 0 && this.#syncDirectReplyLoadingIndicators();
	  }
	  #startDirectReplyRequest(postNumber, lane) {
	    this.#queuedDirectReplyPrefetches.delete(postNumber);
	    const loadDirectReplies = this.#session.loadDirectReplies, post = this.#session.postByNumber(postNumber), expectedCount = Number(post?.reply_count ?? 0);
	    if (!loadDirectReplies || !Number.isSafeInteger(expectedCount) || expectedCount <= 0) {
	      this.#syncDirectReplyLoadingIndicators(), this.#scheduleDirectReplyPrefetchFlush();
	      return;
	    }
	    const request = Object.freeze({
	      lane,
	      controller: new AbortController()
	    });
	    this.#directReplyPrefetches.set(postNumber, request), this.#syncDirectReplyLoadingIndicators(), loadDirectReplies.call(this.#session, postNumber, {
	      background: lane === "nearby",
	      expectedCount,
	      maxPages: DIRECT_REPLY_PREFETCH_MAX_PAGES,
	      signal: request.controller.signal
	    }).then((result) => {
	      if (this.scope.destroyed || this.#directReplyPrefetches.get(postNumber) !== request) return;
	      this.#directReplyPrefetches.delete(postNumber), this.#syncDirectReplyLoadingIndicators();
	      const observedExpectedCount = Math.max(
	        expectedCount,
	        result.expectedCount
	      );
	      this.#directReplyPrefetchAttemptedExpectedCounts.set(
	        postNumber,
	        observedExpectedCount
	      ), result.complete && this.#directReplyPrefetchedExpectedCounts.set(
	        postNumber,
	        observedExpectedCount
	      ), this.frame.notifyScroll();
	    }).catch((error) => {
	      !isAbortFailure(error) && !this.scope.destroyed && this.#directReplyPrefetches.get(postNumber) === request && (this.#directReplyPrefetchAttemptedExpectedCounts.set(
	        postNumber,
	        expectedCount
	      ), this.#onError(error));
	    }).finally(() => {
	      this.#directReplyPrefetches.get(postNumber) === request && this.#directReplyPrefetches.delete(postNumber), this.#syncDirectReplyLoadingIndicators(), this.#scheduleDirectReplyPrefetchFlush();
	    });
	  }
	  #abortDirectReplyRequestsOutside(candidatePostNumbers, visiblePostNumbers) {
	    const visibleRequestPending = [...visiblePostNumbers].some(
	      (postNumber) => this.#directReplyPrefetchRequired(postNumber)
	    );
	    for (const [postNumber, request] of this.#directReplyPrefetches) {
	      if (visibleRequestPending) {
	        if (visiblePostNumbers.has(postNumber) && request.lane === "visible") continue;
	      } else if (candidatePostNumbers.has(postNumber))
	        continue;
	      this.#directReplyPrefetches.delete(postNumber), request.controller.signal.aborted || request.controller.abort(new DOMException(
	        visiblePostNumbers.has(postNumber) ? `树状回复 #${postNumber} 升级为可见快车道` : candidatePostNumbers.has(postNumber) ? `树状回复 #${postNumber} 为可见快车道让位` : `树状回复 #${postNumber} 已滚出当前视口`,
	        "AbortError"
	      ));
	    }
	  }
	  #syncDirectReplyLoadingIndicators() {
	    for (const view of this.domOwner.views()) {
	      const active = this.#directReplyPrefetches.has(view.postNumber), queued = this.#queuedDirectReplyPrefetches.has(view.postNumber), existing = Array.from(view.slots.replyControls.children).find(
	        (child) => child.classList.contains("ldp-direct-reply-loading")
	      );
	      if (!active && !queued) {
	        existing?.remove(), view.slots.replyTree.removeAttribute("aria-busy");
	        continue;
	      }
	      const indicator = existing ?? view.slots.root.ownerDocument.createElement("div");
	      existing || (indicator.className = "ldp-direct-reply-loading", indicator.setAttribute("role", "status"), indicator.setAttribute("aria-live", "polite"), view.slots.replyControls.append(indicator)), indicator.dataset.state = active ? "loading" : "queued", indicator.textContent = active ? "正在优先加载此处树状回复…" : "此处还有树状回复,等待加载…", view.slots.replyTree.setAttribute("aria-busy", "true");
	    }
	  }
	  #activateBranch(root, postNumber) {
	    this.#activeBranchPostNumbers.has(postNumber) || root.classList.contains("ldp-post-projection-pending") || (this.#activeBranchPostNumbers.add(postNumber), this.postProjector.attach(root, postNumber, "branch"));
	  }
	  #deactivateBranch(root, postNumber) {
	    this.#activeBranchPostNumbers.delete(postNumber) && this.postProjector.detach(root, postNumber, "branch");
	  }
	  #scheduleBranchPaint() {
	    this.scope.destroyed || (this.#branchPaintGeneration += 1, this.streamView.slots.rootList.classList.add(
	      "ldp-branch-paint-pending"
	    ), this.#branchPaintHandle === null && this.#queueBranchPaintStabilityCheck());
	  }
	  #queueBranchPaintStabilityCheck() {
	    const generation = this.#branchPaintGeneration;
	    this.#branchPaintHandle = this.#branchFrames.request(() => {
	      this.#branchPaintHandle = null, !this.scope.destroyed && (this.branchOverlay.paint(), this.streamView.slots.rootList.classList.remove(
	        "ldp-branch-paint-pending"
	      ), generation !== this.#branchPaintGeneration && (this.streamView.slots.rootList.classList.add(
	        "ldp-branch-paint-pending"
	      ), this.#queueBranchPaintStabilityCheck()));
	    });
	  }
	  #syncProjectionFeatures() {
	    this.postProjector.syncProjection();
	  }
	  #syncRootObservers(attachedRoots, detachedRoots) {
	    for (const postNumber of detachedRoots) {
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      root && this.#deactivateBranch(root, postNumber), this.#rootObservers.get(postNumber)?.(), this.#rootObservers.delete(postNumber);
	    }
	    for (const postNumber of attachedRoots) {
	      if (this.#rootObservers.has(postNumber)) continue;
	      const root = this.domOwner.view(postNumber)?.slots.root;
	      root && (this.#rootObservers.set(postNumber, this.frame.observeRoot(root)), this.#activateBranch(root, postNumber));
	    }
	  }
	  #assertActive() {
	    if (this.#destroyed || this.scope.destroyed)
	      throw new Error("ReaderTopicDomCoordinator 已销毁");
	  }
	}
}, "117a5b405d2951b7f9a2d37c694a43cc601d45003dc43737ca729b5c17f227d1");

/* Source: lite/src/topic/reader-topic-edit-controller.ts */
runtime.register("src/topic/reader-topic-edit-controller.js", function(module, exports, require) {
	var reader_topic_edit_controller_exports = {};
	__export(reader_topic_edit_controller_exports, {
	  ReaderTopicEditController: () => ReaderTopicEditController
	});
	module.exports = __toCommonJS(reader_topic_edit_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_value_record = require("../kernel/value-record.js"), import_topic_action_feature_commands = require("../post/topic-action-feature-commands.js");
	function text(value) {
	  return String(value ?? "").trim();
	}
	function topicCanEdit(value) {
	  return (0, import_value_record.valueRecord)((0, import_value_record.valueRecord)(value)?.details)?.can_edit === !0;
	}
	function categoryName(value) {
	  return text(value).replace(/\s*[,,]\s*Lv\s*\d+\s*$/i, "").trim();
	}
	function categoryLevel(value) {
	  const match = text(value).match(/(?:^|[,,]\s*)Lv\s*(\d+)\s*$/i);
	  return match?.[1] ? `Lv${match[1]}` : "";
	}
	function topicTags(value) {
	  const tags = (0, import_value_record.valueRecord)(value)?.tags;
	  if (!Array.isArray(tags)) return Object.freeze([]);
	  const byName = /* @__PURE__ */ new Map();
	  for (const value2 of tags) {
	    const source = (0, import_value_record.valueRecord)(value2), name = text(source?.name ?? source?.text ?? value2);
	    if (!name) continue;
	    const id = Number(source?.id);
	    byName.set(name.toLocaleLowerCase(), Object.freeze({
	      id: Number.isSafeInteger(id) && id > 0 ? id : null,
	      name
	    }));
	  }
	  return Object.freeze([...byName.values()]);
	}
	class ReaderTopicEditController {
	  topicId;
	  scope;
	  #session;
	  #trigger;
	  #form;
	  #catalog;
	  #actions;
	  #commands;
	  #descriptors;
	  #models;
	  #onError;
	  #notify;
	  #abort = new AbortController();
	  #opening = null;
	  constructor(options) {
	    this.topicId = (0, import_identifiers.discourseTopicId)(options.topicId), this.#session = options.session, this.#trigger = options.trigger, this.#form = options.form, this.#catalog = options.catalog, this.#actions = options.actions, this.#commands = new import_topic_action_feature_commands.TopicActionFeatureCommands({
	      topicId: this.topicId,
	      session: this.#session
	    }), this.#descriptors = options.descriptors, this.#models = options.models, this.#onError = options.onError ?? (() => {
	    }), this.#notify = options.notify ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.#abort.abort(new Error("Reader Topic 编辑生命周期已结束")), this.#trigger.hidden = !0, this.#trigger.setAttribute("aria-expanded", "false");
	    }), this.scope.listen(this.#trigger, "click", () => {
	      this.open().catch((error) => this.#report(error));
	    }), this.#session.changes.subscribe(() => this.sync(), this.scope), this.sync();
	  }
	  sync() {
	    const canEdit = topicCanEdit(this.#session.topic);
	    this.#trigger.hidden = !canEdit, this.#trigger.disabled = !canEdit, canEdit || this.#trigger.setAttribute("aria-expanded", "false");
	  }
	  open() {
	    if (this.#opening) return this.#opening;
	    const topic = this.#topic();
	    if (!topicCanEdit(topic))
	      return Promise.reject(new Error("当前账号没有编辑该帖子的权限"));
	    const categories = [...this.#catalog.categories()], categoryId = Number(topic.category_id ?? topic.categoryId);
	    Number.isSafeInteger(categoryId) && categoryId > 0 && !categories.some((category) => category.id === categoryId) && categories.unshift(Object.freeze({
	      id: categoryId,
	      name: categoryName(
	        topic.category_name ?? topic.categoryName ?? "当前类别"
	      ) || "当前类别",
	      slug: text(topic.category_slug ?? topic.categorySlug),
	      color: "",
	      parentCategoryId: null
	    })), this.#trigger.setAttribute("aria-expanded", "true");
	    const opening = this.#form.open({
	      title: text(topic.title ?? topic.fancy_title),
	      categoryId: Number.isSafeInteger(categoryId) && categoryId > 0 ? categoryId : 0,
	      tags: topicTags(topic),
	      categories: Object.freeze(categories),
	      signal: this.#abort.signal,
	      searchTags: (input) => this.#catalog.searchTags(input),
	      submit: (submission) => this.#submit(submission)
	    }).finally(() => {
	      this.#opening === opening && (this.#opening = null), this.#trigger.setAttribute("aria-expanded", "false");
	    });
	    return this.#opening = opening, opening;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #submit(submission) {
	    const topic = this.#topic();
	    if (!topicCanEdit(topic))
	      throw new Error("当前账号没有编辑该帖子的权限");
	    const nativeFields = Object.freeze({
	      title: submission.title,
	      category_id: submission.category.id,
	      tags: Object.freeze(submission.tags.map((tag) => Object.freeze({
	        name: tag.name,
	        ...tag.id ? { id: tag.id } : {}
	      })))
	    }), canonicalFields = Object.freeze({
	      title: submission.title,
	      category_id: submission.category.id,
	      category_name: categoryName(submission.category.name),
	      category_level: categoryLevel(submission.category.name),
	      category_slug: submission.category.slug,
	      tags: Object.freeze(submission.tags.map((tag) => tag.name))
	    });
	    await this.#actions.dispatch(this.#commands.edit(
	      canonicalFields,
	      this.#descriptors.topicEdit({
	        topicId: this.topicId,
	        topic: this.#models.createTopic(topic),
	        changedFields: nativeFields
	      })
	    )), this.#notify("帖子信息已更新");
	  }
	  #topic() {
	    const topic = this.#session.topic;
	    if (!topic) throw new Error("canonical Topic 尚未加载");
	    if ((0, import_identifiers.discourseTopicId)(topic.id) !== this.topicId)
	      throw new Error("Topic 编辑目标与当前会话不一致");
	    return topic;
	  }
	  #report(error) {
	    try {
	      this.#onError(error);
	    } catch {
	    }
	  }
	}
}, "f4a30971ecaa7f26c1382c1cc09a7d2ed29c1e375c2dc45e58bfcda8ea5d5547");

/* Source: lite/src/topic/reader-topic-factory.ts */
runtime.register("src/topic/reader-topic-factory.js", function(module, exports, require) {
	var reader_topic_factory_exports = {};
	__export(reader_topic_factory_exports, {
	  createReaderTopicFactory: () => createReaderTopicFactory
	});
	module.exports = __toCommonJS(reader_topic_factory_exports);
	var import_reader_topic_dom_coordinator = require("./reader-topic-dom-coordinator.js");
	function throwIfAborted(signal) {
	  if (signal.aborted) throw signal.reason;
	}
	function createTopicLifetimeContext(context) {
	  const controller = context.scope.abortController(
	    new DOMException(
	      `Topic ${context.topicId} 生命周期已结束`,
	      "AbortError"
	    ),
	    context.signal
	  );
	  return Object.freeze({
	    ...context,
	    signal: controller.signal
	  });
	}
	function createReaderTopicFactory(options) {
	  return async (context) => {
	    throwIfAborted(context.signal);
	    const topicContext = createTopicLifetimeContext(context);
	    options.onPhase?.("prepare", topicContext);
	    const bundle = await options.createBundle(topicContext);
	    bundle.cleanup && topicContext.scope.add(bundle.cleanup), throwIfAborted(topicContext.signal);
	    const root = options.createRoot?.(topicContext, options.document) ?? options.document.createElement("section");
	    if (root.isConnected || root.parentNode)
	      throw new Error("Topic root 在 mount 前必须是 detached");
	    root.classList.contains("ldp-topic-runtime") || root.classList.add("ldp-topic-runtime"), root.dataset.topicId = String(topicContext.topicId), topicContext.mount(root);
	    const dom = new import_reader_topic_dom_coordinator.ReaderTopicDomCoordinator({
	      ...options.createDomOptions(bundle, topicContext, root),
	      document: options.document,
	      topicHost: root,
	      session: bundle.session,
	      replies: bundle.replies,
	      parentScope: topicContext.scope
	    }), topic = await dom.initialize();
	    throwIfAborted(topicContext.signal), options.onPhase?.("render", topicContext);
	    const value = Object.freeze({
	      topic,
	      root,
	      session: bundle.session,
	      replies: bundle.replies,
	      dom,
	      services: bundle.services
	    }), assembledCleanup = options.onAssembled?.(value, topicContext);
	    typeof assembledCleanup == "function" && topicContext.scope.add(assembledCleanup), throwIfAborted(topicContext.signal);
	    const activationCleanup = bundle.activate?.();
	    typeof activationCleanup == "function" && topicContext.scope.add(activationCleanup), throwIfAborted(topicContext.signal);
	    const readyCleanup = options.onReady?.(value, topicContext);
	    return typeof readyCleanup == "function" && topicContext.scope.add(readyCleanup), Object.freeze({
	      value,
	      ...bundle.prepareClose ? { prepareClose: bundle.prepareClose } : {}
	    });
	  };
	}
}, "6bfc20f304a3e8ed6007bc10d14ac7cf18298b671f8063a34860556fc2dbc103");

/* Source: lite/src/topic/reader-topic-flow-controller.ts */
runtime.register("src/topic/reader-topic-flow-controller.js", function(module, exports, require) {
	var reader_topic_flow_controller_exports = {};
	__export(reader_topic_flow_controller_exports, {
	  ReaderTopicFlowController: () => ReaderTopicFlowController
	});
	module.exports = __toCommonJS(reader_topic_flow_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	const browserScheduler = Object.freeze({
	  schedule(callback, urgency, delayMs = urgency === "near-window" ? 0 : 600) {
	    return window.setTimeout(callback, delayMs);
	  },
	  cancel(handle) {
	    window.clearTimeout(handle);
	  }
	}), BACKGROUND_PREFETCH_DELAY_MS = 360, GAP_TARGET_SETTLE_MS = 180;
	class ReaderTopicFlowController {
	  scope;
	  #dom;
	  #readPerformance;
	  #scheduler;
	  #onError;
	  #readLoadDone;
	  #scheduledHandle = null;
	  #scheduledUrgency = null;
	  #running = !1;
	  #rerun = !1;
	  #done = !1;
	  #projectionPriority = !1;
	  #lastUserDrivenLoadAt = 0;
	  #aroundSettleHandle = null;
	  #aroundSettleKey = "";
	  #settledAroundKey = "";
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#dom = options.dom, this.#readPerformance = options.readPerformance, this.#scheduler = options.scheduler ?? browserScheduler, this.#onError = options.onError ?? (() => {
	    }), this.#readLoadDone = options.readLoadDone ?? (() => {
	    }), this.#done = this.#readLoadDone() === !0, this.#syncStatus(!1), this.#dom.windowChanges.subscribe(() => {
	      this.#scheduleCurrentWork();
	    }, this.scope), options.sessionChanges?.subscribe((commit) => {
	      commit.streamChanged && (this.#readLoadDone() !== !0 && (this.#done = !1), this.#syncStatus(!1), this.#scheduleCurrentWork());
	    }, this.scope), this.scope.add(() => {
	      this.#cancelAroundSettle(), this.#scheduledHandle !== null && this.#scheduler.cancel(this.#scheduledHandle), this.#scheduledHandle = null, this.#scheduledUrgency = null;
	    }), this.#scheduleCurrentWork();
	  }
	  refreshPerformance() {
	    this.scope.destroyed || (this.#dom.flushNow(), this.#hasWork() && this.#queue(this.#urgency(), !0));
	  }
	  /**
	   * 需要完整 Topic 投影的功能(目前为“只看楼主”)只提升 canonical Flow,
	   * 不创建第二个 cursor、帖子缓存或请求循环。
	   */
	  setProjectionPriority(enabled) {
	    this.scope.destroyed || this.#projectionPriority === enabled || (this.#projectionPriority = enabled, this.#dom.flushNow(), this.#hasWork() && this.#queue(this.#urgency(), !0));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #rangeHydrationPlan(allowUnsettledAround = !1) {
	    if (!this.#dom.hydrateUnloadedRange) return null;
	    const commit = this.#dom.frame.lastCommit;
	    if (!commit) return null;
	    const { window: window2 } = commit, userScrollAt = this.#userScrollAt();
	    if (userScrollAt <= this.#lastUserDrivenLoadAt) return null;
	    const visibleDataGapPostNumber = this.#dom.visibleDataGapPostNumber?.();
	    if (visibleDataGapPostNumber !== void 0)
	      return Object.freeze({
	        distance: 0,
	        request: Object.freeze({
	          direction: "around",
	          postNumber: visibleDataGapPostNumber
	        }),
	        userScrollAt
	      });
	    const gapTargetPostNumber = window2.unloadedGapTargetPostNumber;
	    if (gapTargetPostNumber !== void 0) {
	      const key = `${userScrollAt}|${gapTargetPostNumber}`, plan = Object.freeze({
	        distance: 0,
	        request: Object.freeze({
	          direction: "around",
	          postNumber: gapTargetPostNumber
	        }),
	        requiresSettle: !0,
	        userScrollAt
	      });
	      return allowUnsettledAround || this.#settledAroundKey === key ? plan : null;
	    }
	    const input = this.#dom.readWindowInput(), performance = this.#readPerformance(), scrollDirection = this.#dom.lastUserScrollDirection?.() ?? 0, beforeHorizon = input.viewportSize * Math.max(
	      performance.nestedPrefetchScreens,
	      Number(input.overscanBeforeScreens) || 0
	    ), afterHorizon = input.viewportSize * Math.max(
	      performance.nestedPrefetchScreens,
	      Number(input.overscanAfterScreens) || 0
	    ), candidates = [], beforeAnchor = window2.unloadedGapBeforeAnchorPostNumber ?? window2.segmentStartPostNumber, beforeDistance = window2.distanceToSegmentStart ?? Number.POSITIVE_INFINITY;
	    window2.hasUnloadedGapBefore === !0 && beforeAnchor !== void 0 && beforeDistance <= beforeHorizon && candidates.push({
	      distance: beforeDistance,
	      request: Object.freeze({
	        direction: "before",
	        postNumber: beforeAnchor
	      }),
	      userScrollAt
	    });
	    const afterAnchor = window2.unloadedGapAfterAnchorPostNumber ?? window2.segmentEndPostNumber, afterDistance = window2.distanceToSegmentEnd ?? Number.POSITIVE_INFINITY;
	    return window2.hasUnloadedGapAfter === !0 && afterAnchor !== void 0 && afterDistance <= afterHorizon && candidates.push({
	      distance: afterDistance,
	      request: Object.freeze({
	        direction: "after",
	        postNumber: afterAnchor
	      }),
	      userScrollAt
	    }), candidates.filter(
	      (candidate) => scrollDirection === 0 || scrollDirection < 0 && candidate.request.direction === "before" || scrollDirection > 0 && candidate.request.direction === "after"
	    ).sort((left, right) => left.distance - right.distance)[0] ?? null;
	  }
	  #sequentialLoadAllowed() {
	    if (this.#done || this.#dom.visibleDataGapPostNumber?.() !== void 0) return !1;
	    const window2 = this.#dom.frame.lastCommit?.window;
	    return window2?.unloadedGapTargetPostNumber !== void 0 || window2?.hasUnloadedGapBefore === !0 ? !1 : this.#projectionPriority ? !0 : this.#userScrollAt() > this.#lastUserDrivenLoadAt;
	  }
	  #userScrollAt() {
	    return Math.max(0, Number(this.#dom.lastUserScrollAt?.()) || 0);
	  }
	  #consumeUserScrollIntent(observedAt = this.#userScrollAt()) {
	    this.#lastUserDrivenLoadAt = Math.max(
	      this.#lastUserDrivenLoadAt,
	      observedAt
	    );
	  }
	  #scheduleCurrentWork() {
	    if (this.scope.destroyed) return;
	    const rawRangePlan = this.#rangeHydrationPlan(!0);
	    if (rawRangePlan?.requiresSettle === !0 && this.#settledAroundKey !== `${rawRangePlan.userScrollAt}|${rawRangePlan.request.postNumber}`) {
	      this.#scheduleAroundSettle(rawRangePlan);
	      return;
	    }
	    this.#cancelAroundSettle(), this.#hasWork() && this.#queue(this.#urgency());
	  }
	  #scheduleAroundSettle(plan) {
	    const key = `${plan.userScrollAt}|${plan.request.postNumber}`;
	    this.#aroundSettleHandle !== null && key === this.#aroundSettleKey || (this.#cancelAroundSettle(), this.#scheduledHandle !== null && (this.#scheduler.cancel(this.#scheduledHandle), this.#scheduledHandle = null, this.#scheduledUrgency = null), this.#aroundSettleKey = key, this.#aroundSettleHandle = this.#scheduler.schedule(() => {
	      this.#aroundSettleHandle = null;
	      const current = this.#rangeHydrationPlan(!0), currentKey = current?.requiresSettle === !0 ? `${current.userScrollAt}|${current.request.postNumber}` : "";
	      if (!current || currentKey !== this.#aroundSettleKey) {
	        this.#aroundSettleKey = "", this.#scheduleCurrentWork();
	        return;
	      }
	      this.#aroundSettleKey = "", this.#settledAroundKey = currentKey, this.#hasWork() && this.#queue("near-window", !0);
	    }, "near-window", GAP_TARGET_SETTLE_MS));
	  }
	  #cancelAroundSettle() {
	    this.#aroundSettleHandle !== null && this.#scheduler.cancel(this.#aroundSettleHandle), this.#aroundSettleHandle = null, this.#aroundSettleKey = "";
	  }
	  #hasWork() {
	    return this.#rangeHydrationPlan() !== null || this.#sequentialLoadAllowed();
	  }
	  #urgency() {
	    if (this.#rangeHydrationPlan() || this.#projectionPriority || this.#dom.hasVisibleDataGap?.() === !0) return "near-window";
	    const commit = this.#dom.frame.lastCommit;
	    if (!commit) return "near-window";
	    if (commit.window.unloadedGapTargetPostNumber !== void 0 || commit.window.hasUnloadedGapBefore === !0) return "background";
	    const input = this.#dom.readWindowInput(), forwardScreens = Math.max(
	      this.#readPerformance().nestedPrefetchScreens,
	      Number(input.overscanAfterScreens) || 0
	    ), horizon = input.viewportSize * forwardScreens;
	    return (commit.window.afterSegmentSpacer ?? commit.window.afterSpacer) <= horizon ? "near-window" : "background";
	  }
	  #queue(urgency, replace = !1, delayOverrideMs) {
	    if (this.scope.destroyed || !this.#hasWork()) return;
	    if (this.#running) {
	      this.#rerun = !0;
	      return;
	    }
	    if (this.#scheduledHandle !== null) {
	      const promote = this.#scheduledUrgency === "background" && urgency === "near-window";
	      if (!replace && !promote) return;
	      this.#scheduler.cancel(this.#scheduledHandle);
	    }
	    this.#scheduledUrgency = urgency;
	    const delayMs = delayOverrideMs ?? (urgency === "near-window" ? 0 : Math.max(
	      BACKGROUND_PREFETCH_DELAY_MS,
	      this.#readPerformance().requestMinIntervalMs * 3
	    ));
	    this.#scheduledHandle = this.#scheduler.schedule(() => {
	      this.#scheduledHandle = null, this.#scheduledUrgency = null, this.#run();
	    }, urgency, delayMs);
	  }
	  async #run() {
	    if (this.scope.destroyed || this.#running || !this.#hasWork()) return;
	    this.#running = !0, this.#rerun = !1;
	    const rangePlan = this.#rangeHydrationPlan(), urgency = rangePlan ? "near-window" : this.#urgency();
	    let activeRange = !1, loadingPublished = !1, failed = !1, sequentialUserScrollAt = null;
	    try {
	      if (rangePlan && this.#dom.hydrateUnloadedRange) {
	        if (activeRange = !0, this.#consumeUserScrollIntent(rangePlan.userScrollAt), await this.#dom.hydrateUnloadedRange(rangePlan.request, {
	          background: !1,
	          priority: "visible",
	          maxAttempts: 1
	        }), this.scope.destroyed) return;
	        this.#dom.flushNow();
	        return;
	      }
	      if (!this.#sequentialLoadAllowed())
	        return;
	      this.#projectionPriority || (sequentialUserScrollAt = this.#userScrollAt(), this.#consumeUserScrollIntent(sequentialUserScrollAt)), loadingPublished = urgency === "near-window", loadingPublished && this.#syncStatus(!0);
	      const visibleDataGap = this.#dom.hasVisibleDataGap?.() === !0;
	      let source = null;
	      const load = this.#dom.loadNext({
	        background: urgency === "background",
	        priority: visibleDataGap ? "nested" : "visible",
	        maxAttempts: 1,
	        onSource(nextSource) {
	          source = nextSource;
	        }
	      }), aheadBatchCount = urgency === "near-window" ? Math.min(
	        2,
	        Math.max(0, this.#readPerformance().requestMaxConcurrent - 1)
	      ) : 0;
	      source !== null && aheadBatchCount > 0 && this.#dom.prefetchAhead && this.#dom.prefetchAhead(aheadBatchCount).catch(() => {
	      });
	      const result = await load;
	      if (this.scope.destroyed) return;
	      if (this.#done = this.#readLoadDone() ?? result.done, this.#dom.flushNow(), loadingPublished && this.#syncStatus(!1), result.fatal) {
	        failed = !0;
	        return;
	      }
	      if (result.retry) {
	        failed = !0;
	        return;
	      }
	      if (!this.#done) {
	        const nextUrgency = this.#urgency();
	        nextUrgency === "near-window" && this.#queue(nextUrgency);
	      }
	    } catch (error) {
	      this.scope.destroyed || (failed = !0, activeRange ? this.#consumeUserScrollIntent() : sequentialUserScrollAt !== null && this.#consumeUserScrollIntent(), !activeRange && loadingPublished && this.#syncStatus(!1), this.#onError(error));
	    } finally {
	      if (this.#running = !1, !failed && this.#rerun && !this.scope.destroyed && this.#hasWork()) {
	        const nextUrgency = this.#urgency();
	        nextUrgency === "near-window" && this.#queue(nextUrgency, !0);
	      }
	    }
	  }
	  #syncStatus(loading) {
	    this.#dom.setFlowStatus?.(Object.freeze({
	      loading,
	      done: !loading && this.#done
	    }));
	  }
	}
}, "b7e12fd69dbaf85e3408e260697e3b25c10dfb8fb685bd3e8eac31144ba2a376");

/* Source: lite/src/topic/reader-topic-header.ts */
runtime.register("src/topic/reader-topic-header.js", function(module, exports, require) {
	var reader_topic_header_exports = {};
	__export(reader_topic_header_exports, {
	  ReaderTopicHeaderController: () => ReaderTopicHeaderController,
	  ReaderTopicHeaderView: () => ReaderTopicHeaderView,
	  clearReaderTopicHostIdentityCache: () => clearReaderTopicHostIdentityCache,
	  normalizeReaderTopicHeader: () => normalizeReaderTopicHeader,
	  readReaderTopicHostIconMetadata: () => readReaderTopicHostIconMetadata,
	  readerTopicHostIdentityCacheStats: () => readerTopicHostIdentityCacheStats,
	  readerTopicOwnerUsername: () => readerTopicOwnerUsername
	});
	module.exports = __toCommonJS(reader_topic_header_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_value_record = require("../kernel/value-record.js");
	const EMPTY_RECORD = Object.freeze({});
	function text(value) {
	  return String(value ?? "").trim();
	}
	function count(value) {
	  if (value == null || value === "") return null;
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
	}
	function categoryLevel(value) {
	  const match = text(value).match(/(?:^|[,,]\s*)Lv\s*(\d+)\s*$/i);
	  return match?.[1] ? `Lv${match[1]}` : "";
	}
	function categoryName(value) {
	  return text(value).replace(/\s*[,,]\s*Lv\s*\d+\s*$/i, "").trim();
	}
	function safeIconName(value) {
	  const normalized = text(value).toLowerCase().replace(/^fa-/, "");
	  return normalized.length <= 80 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized) ? normalized : "";
	}
	function topicTags(topic) {
	  if (!Array.isArray(topic.tags)) return Object.freeze([]);
	  const tags = topic.tags.map((value) => {
	    const source = (0, import_value_record.valueRecord)(value);
	    return Object.freeze({
	      name: text(source?.name ?? source?.id ?? value),
	      icon: safeIconName(
	        source?.icon ?? source?.icon_name ?? source?.iconName
	      )
	    });
	  }).filter((tag) => !!tag.name), byName = /* @__PURE__ */ new Map();
	  for (const tag of tags) {
	    const previous = byName.get(tag.name);
	    (!previous || !previous.icon && tag.icon) && byName.set(tag.name, tag);
	  }
	  return Object.freeze([...byName.values()]);
	}
	function readerTopicOwnerUsername(topicValue, posts = []) {
	  const topic = (0, import_value_record.valueRecord)(topicValue) ?? EMPTY_RECORD, details = (0, import_value_record.valueRecord)(topic.details), createdBy = (0, import_value_record.valueRecord)(details?.created_by), firstPost = posts.map((post) => (0, import_value_record.valueRecord)(post)).find((post) => Number(post?.post_number) === 1);
	  return text(
	    topic._opUsername || createdBy?.username || firstPost?.username || topic.original_poster_username
	  );
	}
	function normalizeReaderTopicHeader(topicValue, posts, presentation, fallbackTopicId = 0) {
	  const topic = (0, import_value_record.valueRecord)(topicValue) ?? EMPTY_RECORD, category = (0, import_value_record.valueRecord)(topic.category) ?? EMPTY_RECORD, owner = readerTopicOwnerUsername(topic, posts), categoryId = count(
	    topic.category_id ?? topic.categoryId ?? category.id
	  ) ?? 0, rawCategoryName = topic.category_name ?? topic.categoryName ?? category.name ?? topic.category_slug ?? category.slug ?? presentation.categoryName?.(categoryId), normalizedCategoryName = categoryName(rawCategoryName), level = [
	    topic.category_level,
	    topic.categoryLevel,
	    rawCategoryName
	  ].map(categoryLevel).find(Boolean) ?? "", tags = topicTags(topic).map((tag) => Object.freeze({
	    name: tag.name,
	    icon: tag.icon,
	    href: presentation.tagHref(tag.name)
	  })), voteCount = count(topic.vote_count) ?? 0, voted = topic.user_voted === !0, canVote = topic.can_vote === !0 || voted, hasVoteCapability = ["can_vote", "user_voted", "vote_count"].some((key) => Object.hasOwn(topic, key)), stats = [], postsCount = count(topic.posts_count), views = count(topic.views), likes = count(topic.like_count), participants = count(topic.participant_count);
	  return postsCount !== null && stats.push(`${postsCount} 帖`), views !== null && stats.push(`${views} 浏览`), likes !== null && stats.push(`${likes} 赞`), participants !== null && stats.push(`${participants} 用户`), Object.freeze({
	    topicId: count(topic.id) ?? count(fallbackTopicId) ?? 0,
	    categoryId,
	    title: text(topic.title ?? topic.fancy_title) || "未命名主题",
	    ownerUsername: owner,
	    ownerHref: presentation.userHref(owner),
	    statsText: stats.join(" · ") || "主题信息暂不可用",
	    category: normalizedCategoryName ? Object.freeze({
	      id: categoryId,
	      name: normalizedCategoryName,
	      level,
	      icon: safeIconName(
	        topic.category_icon ?? topic.categoryIcon ?? category.icon ?? category.icon_name ?? category.iconName ?? presentation.categoryIcon?.(categoryId)
	      ),
	      href: presentation.categoryHref(categoryId)
	    }) : null,
	    tags: Object.freeze(tags),
	    vote: hasVoteCapability && (canVote || voteCount > 0) ? Object.freeze({ count: voteCount, voted, canVote }) : null
	  });
	}
	class ReaderTopicHeaderController {
	  scope;
	  changes = new import_signal.Signal();
	  #session;
	  #presentation;
	  #topicId;
	  #onError;
	  #snapshot;
	  constructor(options) {
	    this.#session = options.session, this.#presentation = options.presentation, this.#topicId = count(options.session.topicId) ?? 0, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#snapshot = normalizeReaderTopicHeader(
	      this.#session.topic,
	      this.#session.cachedPosts(),
	      this.#presentation,
	      this.#topicId
	    ), this.#session.changes.subscribe(() => this.refresh(), this.scope), this.scope.add(() => this.changes.clear());
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  refresh() {
	    const next = normalizeReaderTopicHeader(
	      this.#session.topic,
	      this.#session.cachedPosts(),
	      this.#presentation,
	      this.#topicId
	    );
	    if (JSON.stringify(next) === JSON.stringify(this.#snapshot))
	      return this.#snapshot;
	    this.#snapshot = next;
	    for (const error of this.changes.emit(next)) this.#onError(error);
	    return next;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
	function appendLinkedIdentity(document, parent, className, label, href, icon, renderIcon, hostIcon = null) {
	  const node = document.createElement(href ? "a" : "span");
	  node.className = className, node.tagName === "A" && href && node.setAttribute("href", href);
	  const fallbackIcon = className.includes("ldp-topic-category") ? "code" : "tag", hostIconName = safeIconName(
	    hostIcon?.querySelector("use")?.getAttribute("href")?.replace(/^#/, "")
	  ), requestedIcon = icon || hostIconName;
	  if (requestedIcon) {
	    const rendered = (0, import_reader_icon.renderReaderIcon)(document, requestedIcon, renderIcon), renderedElement = rendered.nodeType === 1 ? rendered : null, unresolved = renderedElement?.matches(
	      "[data-reader-icon-fallback-for]"
	    ) || renderedElement?.querySelector(
	      "[data-reader-icon-fallback-for]"
	    );
	    node.append(
	      unresolved ? (0, import_reader_icon.createReaderIcon)(document, fallbackIcon) : rendered
	    );
	  } else hostIcon && hostIcon.tagName.toLowerCase() === "img" ? node.append(hostIcon) : node.append((0, import_reader_icon.createReaderIcon)(document, fallbackIcon));
	  const textNode = document.createElement("span");
	  return textNode.className = "ldp-topic-tag-text", textNode.textContent = label, node.append(textNode), parent.append(node), node;
	}
	const SAFE_ICON_FRAGMENT = /^#[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/, HOST_IDENTITY_CACHE = /* @__PURE__ */ new WeakMap(), HOST_IDENTITY_INDEX_LIMIT = 128;
	function readerTopicHostIdentityCacheStats(document) {
	  const cache = HOST_IDENTITY_CACHE.get(document);
	  return Object.freeze({
	    categoryEntries: cache ? (/* @__PURE__ */ new Set([
	      ...cache.categoryIcons.keys(),
	      ...cache.categoryHrefs.keys()
	    ])).size : 0,
	    tagEntries: cache ? (/* @__PURE__ */ new Set([
	      ...cache.tagIcons.keys(),
	      ...cache.tagHrefs.keys()
	    ])).size : 0,
	    indexLimit: HOST_IDENTITY_INDEX_LIMIT
	  });
	}
	function clearReaderTopicHostIdentityCache(document) {
	  HOST_IDENTITY_CACHE.delete(document);
	}
	function rememberHostIdentity(map, key, value) {
	  for (map.delete(key), map.set(key, value); map.size > HOST_IDENTITY_INDEX_LIMIT; ) {
	    const oldest = map.keys().next().value;
	    if (oldest === void 0) break;
	    map.delete(oldest);
	  }
	}
	function cachedHostIdentity(map, key) {
	  const value = map.get(key);
	  if (value !== void 0)
	    return map.delete(key), map.set(key, value), value;
	}
	function hostIdentityCache(document) {
	  let cache = HOST_IDENTITY_CACHE.get(document);
	  return cache || (cache = {
	    categoryIcons: /* @__PURE__ */ new Map(),
	    categoryHrefs: /* @__PURE__ */ new Map(),
	    tagIcons: /* @__PURE__ */ new Map(),
	    tagHrefs: /* @__PURE__ */ new Map()
	  }, HOST_IDENTITY_CACHE.set(document, cache)), cache;
	}
	function categoryCacheKeys(id, name) {
	  return Object.freeze([
	    ...id > 0 ? [`id:${id}`] : [],
	    ...name ? [`name:${name}`] : []
	  ]);
	}
	function cachedCategoryValue(values, id, name) {
	  for (const key of categoryCacheKeys(id, name)) {
	    const value = cachedHostIdentity(values, key);
	    if (value !== void 0) return value;
	  }
	  return null;
	}
	function cloneHostIdentityIcon(link) {
	  if (!link) return null;
	  const source = [...link.querySelectorAll("svg,img")].find((candidate) => candidate.closest("a") === link);
	  if (!source) return null;
	  const clone = source.cloneNode(!0);
	  clone.querySelectorAll("script,foreignObject").forEach((node) => node.remove());
	  for (const node of [clone, ...clone.querySelectorAll("*")])
	    for (const attribute of [...node.attributes])
	      /^on/i.test(attribute.name) && node.removeAttribute(attribute.name);
	  if (clone.tagName.toLowerCase() === "svg")
	    for (const use of clone.querySelectorAll("use")) {
	      const href = use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? "";
	      if (!SAFE_ICON_FRAGMENT.test(href)) return null;
	    }
	  else {
	    const sourceUrl = clone.getAttribute("src") ?? "";
	    if (/^\s*javascript:/i.test(sourceUrl)) return null;
	    clone.removeAttribute("srcset"), clone.setAttribute("alt", "");
	  }
	  return clone.setAttribute("aria-hidden", "true"), clone;
	}
	function hostTopicRouteId(document) {
	  const pathname = document.defaultView?.location?.pathname ?? "";
	  return Number(
	    pathname.match(/\/t\/(?:[^/]+\/)?(\d+)(?:\/|$)/)?.[1] ?? 0
	  );
	}
	const HOST_TOPIC_SOURCE_ROOT_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item,.search-result-topic,.fps-result,.category-topic-link";
	function hostSourceTopicId(root) {
	  const direct = Number(
	    root.getAttribute("data-topic-id") ?? root.dataset.topicId ?? 0
	  );
	  if (Number.isSafeInteger(direct) && direct > 0) return direct;
	  const href = root.querySelector('a[href*="/t/"]')?.getAttribute("href") ?? "";
	  return Number(
	    href.match(/\/t\/(?:[^/]+\/)?(\d+)(?:\/|$)/)?.[1] ?? 0
	  );
	}
	function readReaderTopicHostIconMetadata(document, snapshot) {
	  const cache = hostIdentityCache(document), tagIcons = /* @__PURE__ */ new Map(), tagHrefs = /* @__PURE__ */ new Map(), empty = () => Object.freeze({
	    categoryIcon: null,
	    categoryId: 0,
	    categoryName: "",
	    categoryLevel: "",
	    categoryHref: "",
	    tagIcons,
	    tagHrefs
	  }), routeTopicId = hostTopicRouteId(document);
	  let root = routeTopicId > 0 && (!snapshot.topicId || routeTopicId === snapshot.topicId) ? [...document.querySelectorAll(
	    "#topic-title,.topic-title,.title-wrapper"
	  )].find((candidate) => !candidate.closest(".ldp-overlay")) ?? null : null;
	  root || (root = [...document.querySelectorAll(
	    HOST_TOPIC_SOURCE_ROOT_SELECTOR
	  )].find(
	    (candidate) => !candidate.closest(".ldp-overlay") && hostSourceTopicId(candidate) === snapshot.topicId
	  ) ?? null);
	  const categorySelector = 'a.badge-category__wrapper,a.badge-category,a.topic-category,a[href^="/c/"],a[href*="/c/"]', categoryLink = root?.querySelector(categorySelector) ?? (snapshot.categoryId > 0 ? [...document.querySelectorAll(categorySelector)].find((candidate) => {
	    const href = candidate.getAttribute("href") ?? "";
	    return (Number(
	      candidate.querySelector(
	        "[data-category-id]"
	      )?.dataset.categoryId
	    ) || Number(
	      href.match(/\/c\/(?:[^/]+\/)*(\d+)(?:\/|$)/)?.[1] ?? 0
	    )) === snapshot.categoryId;
	  }) ?? null : null), tagNames = new Set(snapshot.tags.map((tag) => tag.name));
	  for (const link of (root ?? document).querySelectorAll(
	    "a.discourse-tag,.discourse-tags a,.topic-tags a"
	  )) {
	    const name = text(link.dataset.tagName ?? link.textContent);
	    if (!root && !tagNames.has(name)) continue;
	    const icon = cloneHostIdentityIcon(link), href = link.getAttribute("href") ?? "";
	    name && icon && tagIcons.set(name, icon), name && href && tagHrefs.set(name, href);
	  }
	  for (const [name, icon] of tagIcons)
	    rememberHostIdentity(
	      cache.tagIcons,
	      name,
	      icon.cloneNode(!0)
	    );
	  for (const [name, href] of tagHrefs)
	    rememberHostIdentity(cache.tagHrefs, name, href);
	  for (const tag of snapshot.tags) {
	    if (!tagIcons.has(tag.name)) {
	      const cachedIcon = cachedHostIdentity(cache.tagIcons, tag.name);
	      cachedIcon && tagIcons.set(tag.name, cachedIcon.cloneNode(!0));
	    }
	    if (!tagHrefs.has(tag.name)) {
	      const cachedHref = cachedHostIdentity(cache.tagHrefs, tag.name);
	      cachedHref && tagHrefs.set(tag.name, cachedHref);
	    }
	  }
	  const categoryText = text(
	    categoryLink?.dataset.categoryName ?? categoryLink?.getAttribute("title") ?? categoryLink?.textContent
	  ), liveCategoryHref = categoryLink?.getAttribute("href") ?? "", hrefCategoryId = Number(
	    liveCategoryHref.match(/\/c\/(?:[^/]+\/)*(\d+)(?:\/|$)/)?.[1] ?? 0
	  ), categoryId = Number(categoryLink?.dataset.categoryId) || hrefCategoryId || snapshot.categoryId, normalizedCategoryName = categoryName(categoryText) || snapshot.category?.name || "", liveCategoryIcon = cloneHostIdentityIcon(categoryLink);
	  for (const key of categoryCacheKeys(categoryId, normalizedCategoryName))
	    liveCategoryIcon && rememberHostIdentity(
	      cache.categoryIcons,
	      key,
	      liveCategoryIcon.cloneNode(!0)
	    ), liveCategoryHref && rememberHostIdentity(cache.categoryHrefs, key, liveCategoryHref);
	  const cachedCategoryIcon = cachedCategoryValue(
	    cache.categoryIcons,
	    categoryId,
	    normalizedCategoryName
	  ), categoryIcon = liveCategoryIcon ?? cachedCategoryIcon?.cloneNode(!0) ?? null, categoryHref = liveCategoryHref || cachedCategoryValue(
	    cache.categoryHrefs,
	    categoryId,
	    normalizedCategoryName
	  ) || "";
	  return !root && !categoryLink && !categoryIcon && !categoryHref && !tagIcons.size && !tagHrefs.size ? empty() : Object.freeze({
	    categoryIcon,
	    categoryId,
	    categoryName: normalizedCategoryName,
	    categoryLevel: categoryLevel(categoryText),
	    categoryHref,
	    tagIcons,
	    tagHrefs
	  });
	}
	function hasCompleteHostIdentityIcons(metadata, snapshot) {
	  return (snapshot.category ? !!(snapshot.category.icon || metadata.categoryIcon) : snapshot.categoryId <= 0 || !!(metadata.categoryName && metadata.categoryIcon)) && snapshot.tags.every(
	    (tag) => !!(tag.icon || metadata.tagIcons.get(tag.name))
	  );
	}
	class ReaderTopicHeaderView {
	  scope;
	  #controller;
	  #elements;
	  #onJumpFirst;
	  #onlyOp;
	  #onToggleTopicVote;
	  #renderIcon;
	  #onError;
	  #hostDocument;
	  #topicScroller;
	  #topicTags;
	  #topicVote;
	  #requestFrame;
	  #cancelFrame;
	  #hostMetadataObserver = null;
	  #topicHintFrame = 0;
	  #hostMetadataFrame = 0;
	  #hostMetadataStopTimer = 0;
	  #hostMetadataRetryTimer = 0;
	  #hostMetadataRetryDelay = 80;
	  #topicVotePending = !1;
	  constructor(options) {
	    this.#controller = options.controller, this.#elements = options.elements, this.#onJumpFirst = options.onJumpFirst, this.#onlyOp = options.onlyOp ?? null, this.#onToggleTopicVote = options.onToggleTopicVote ?? null, this.#renderIcon = options.renderIcon ?? null, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const document = this.#elements.topicIdentityHost.ownerDocument, rootHost = this.#elements.topicIdentityHost.getRootNode().host;
	    this.#hostDocument = options.hostDocument ?? rootHost?.ownerDocument ?? document;
	    const defaultView = this.#hostDocument.defaultView ?? document.defaultView;
	    this.#requestFrame = options.requestFrame ?? ((callback) => defaultView?.requestAnimationFrame ? defaultView.requestAnimationFrame(callback) : setTimeout(() => callback(Date.now()), 0)), this.#cancelFrame = options.cancelFrame ?? ((id) => {
	      if (defaultView?.cancelAnimationFrame) {
	        defaultView.cancelAnimationFrame(id);
	        return;
	      }
	      clearTimeout(id);
	    });
	    const leftHint = document.createElement("span");
	    leftHint.className = "ldp-title-topic-scroll-hint ldp-title-topic-scroll-hint-left", leftHint.setAttribute("aria-hidden", "true");
	    const scroller = document.createElement("div");
	    scroller.className = "ldp-title-topic-scroller", scroller.setAttribute("role", "group"), scroller.tabIndex = 0, scroller.setAttribute(
	      "aria-label",
	      "主题分类和标签,滚轮可横向浏览"
	    );
	    const tags = document.createElement("div");
	    tags.className = "ldp-topic-tags";
	    const vote = document.createElement("div");
	    vote.className = "ldp-topic-vote-slot", vote.hidden = !0, scroller.append(tags, vote);
	    const rightHint = document.createElement("span");
	    rightHint.className = "ldp-title-topic-scroll-hint ldp-title-topic-scroll-hint-right", rightHint.setAttribute("aria-hidden", "true"), this.#elements.topicIdentityHost.replaceChildren(
	      leftHint,
	      scroller,
	      rightHint
	    ), this.#topicScroller = scroller, this.#topicTags = tags, this.#topicVote = vote, this.scope.listen(vote, "click", (event) => {
	      const button = event.target?.closest("[data-topic-vote]");
	      !button || button.disabled || (event.preventDefault(), this.#toggleTopicVote());
	    }), this.scope.listen(scroller, "scroll", () => this.#queueTopicHints(), {
	      passive: !0
	    }), this.scope.listen(
	      this.#elements.topicIdentityHost,
	      "wheel",
	      (event) => this.#onTopicWheel(event),
	      { passive: !1 }
	    );
	    const NativeResizeObserver = defaultView?.ResizeObserver, resizeObserver = options.createResizeObserver?.(
	      () => this.#queueTopicHints()
	    ) ?? (NativeResizeObserver ? new NativeResizeObserver(() => this.#queueTopicHints()) : null);
	    resizeObserver && (resizeObserver.observe(this.#elements.topicIdentityHost), resizeObserver.observe(scroller), resizeObserver.observe(tags), this.scope.add(() => resizeObserver.disconnect())), this.scope.add(() => {
	      this.#topicHintFrame && (this.#cancelFrame(this.#topicHintFrame), this.#topicHintFrame = 0), this.#stopHostMetadataHydration();
	    }), this.scope.listen(this.#elements.titleJump, "click", () => {
	      this.#jumpFirst();
	    }), this.scope.listen(this.#elements.titleJump, "keydown", (event) => {
	      const key = event.key;
	      key !== "Enter" && key !== " " || (event.preventDefault(), this.#jumpFirst());
	    }), this.scope.listen(this.#elements.onlyOpToggle, "click", () => {
	      try {
	        this.#onlyOp?.toggle();
	      } catch (error) {
	        this.#onError(error);
	      }
	    }), this.#controller.changes.subscribe(
	      (snapshot) => this.render(snapshot),
	      this.scope
	    ), this.#onlyOp?.changes.subscribe(
	      (snapshot) => this.#renderOnlyOp(snapshot),
	      this.scope
	    ), this.render(this.#controller.snapshot), this.#renderOnlyOp(this.#onlyOp?.snapshot ?? null), this.#startHostMetadataHydration(options.createMutationObserver);
	  }
	  render(snapshot) {
	    const document = this.#elements.titleJump.ownerDocument, hostIcons = readReaderTopicHostIconMetadata(
	      this.#hostDocument,
	      snapshot
	    );
	    this.#elements.titleJump.textContent = snapshot.title, this.#elements.metaStats.textContent = snapshot.statsText, this.#elements.metaOwner.hidden = !snapshot.ownerUsername, this.#elements.metaOwnerValue.textContent = snapshot.ownerUsername ? `@${snapshot.ownerUsername}` : "", snapshot.ownerUsername && snapshot.ownerHref ? (this.#elements.metaOwnerValue.href = snapshot.ownerHref, this.#elements.metaOwnerValue.dataset.userCard = snapshot.ownerUsername, this.#elements.metaOwnerValue.target = "_blank", this.#elements.metaOwnerValue.rel = "noopener") : (this.#elements.metaOwnerValue.removeAttribute("href"), this.#elements.metaOwnerValue.removeAttribute("data-user-card"), this.#elements.metaOwnerValue.removeAttribute("target"), this.#elements.metaOwnerValue.removeAttribute("rel")), this.#topicTags.replaceChildren();
	    const renderedCategory = snapshot.category ? Object.freeze({
	      ...snapshot.category,
	      href: snapshot.category.href || hostIcons.categoryHref
	    }) : hostIcons.categoryName ? Object.freeze({
	      id: hostIcons.categoryId,
	      name: hostIcons.categoryName,
	      level: hostIcons.categoryLevel,
	      icon: "",
	      href: hostIcons.categoryHref
	    }) : null;
	    if (renderedCategory) {
	      const label = renderedCategory.level ? `${renderedCategory.name}, ${renderedCategory.level}` : renderedCategory.name;
	      appendLinkedIdentity(
	        document,
	        this.#topicTags,
	        "ldp-topic-tag ldp-topic-category",
	        label,
	        renderedCategory.href,
	        renderedCategory.icon,
	        this.#renderIcon,
	        hostIcons.categoryIcon
	      );
	    }
	    for (const tag of snapshot.tags)
	      appendLinkedIdentity(
	        document,
	        this.#topicTags,
	        "ldp-topic-tag ldp-topic-label",
	        tag.name,
	        tag.href || hostIcons.tagHrefs.get(tag.name) || "",
	        tag.icon,
	        this.#renderIcon,
	        hostIcons.tagIcons.get(tag.name) ?? null
	      );
	    this.#topicTags.hidden = this.#topicTags.childElementCount === 0, this.#renderTopicVote(snapshot.vote), this.#queueTopicHints(), hasCompleteHostIdentityIcons(hostIcons, snapshot) && !(snapshot.categoryId > 0 && !snapshot.category && !hostIcons.categoryName) && this.#stopHostMetadataHydration();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #startHostMetadataHydration(createMutationObserver) {
	    const refreshed = this.#controller.refresh(), metadata = readReaderTopicHostIconMetadata(
	      this.#hostDocument,
	      refreshed
	    );
	    if (!(refreshed.categoryId > 0 && !refreshed.category && !metadata.categoryName) && hasCompleteHostIdentityIcons(metadata, refreshed)) {
	      this.render(refreshed);
	      return;
	    }
	    const document = this.#hostDocument, root = document.querySelector("#main-outlet,#ember-app") ?? document.body ?? document.documentElement, NativeMutationObserver = document.defaultView?.MutationObserver;
	    if (!root || !createMutationObserver && !NativeMutationObserver) return;
	    const onMutation = (records) => {
	      records.some((record2) => {
	        const target = record2.target;
	        return typeof target.closest != "function" || !target.closest(".ldp-overlay");
	      }) && this.#queueHostMetadataSync();
	    };
	    this.#hostMetadataObserver = createMutationObserver?.(onMutation) ?? (NativeMutationObserver ? new NativeMutationObserver(onMutation) : null), this.#hostMetadataObserver?.observe(root, {
	      childList: !0,
	      subtree: !0
	    }), this.#hostMetadataRetryDelay = 80, this.#queueHostMetadataSync(), this.#hostMetadataStopTimer = document.defaultView?.setTimeout(
	      () => this.#stopHostMetadataHydration(),
	      6e4
	    ) ?? 0;
	  }
	  #queueHostMetadataSync() {
	    this.#hostMetadataFrame || this.scope.destroyed || (this.#hostMetadataFrame = this.#requestFrame(() => {
	      if (this.#hostMetadataFrame = 0, this.scope.destroyed) return;
	      const previous = this.#controller.snapshot, next = this.#controller.refresh();
	      next === previous && this.render(next), this.#hostMetadataObserver && this.#scheduleHostMetadataRetry();
	    }));
	  }
	  #scheduleHostMetadataRetry() {
	    if (this.#hostMetadataRetryTimer || !this.#hostMetadataObserver) return;
	    const window = this.#hostDocument.defaultView;
	    if (!window) return;
	    const delay = this.#hostMetadataRetryDelay;
	    this.#hostMetadataRetryDelay = Math.min(delay * 2, 4e3), this.#hostMetadataRetryTimer = window.setTimeout(() => {
	      this.#hostMetadataRetryTimer = 0, this.#queueHostMetadataSync();
	    }, delay);
	  }
	  #stopHostMetadataHydration() {
	    this.#hostMetadataObserver?.disconnect(), this.#hostMetadataObserver = null, this.#hostMetadataFrame && (this.#cancelFrame(this.#hostMetadataFrame), this.#hostMetadataFrame = 0), this.#hostMetadataStopTimer && (this.#hostDocument.defaultView?.clearTimeout(
	      this.#hostMetadataStopTimer
	    ), this.#hostMetadataStopTimer = 0), this.#hostMetadataRetryTimer && (this.#hostDocument.defaultView?.clearTimeout(
	      this.#hostMetadataRetryTimer
	    ), this.#hostMetadataRetryTimer = 0);
	  }
	  async #jumpFirst() {
	    try {
	      await this.#onJumpFirst();
	    } catch (error) {
	      this.scope.destroyed || this.#onError(error);
	    }
	  }
	  async #toggleTopicVote() {
	    const vote = this.#controller.snapshot.vote;
	    if (!(!vote || !this.#onToggleTopicVote || this.#topicVotePending)) {
	      this.#topicVotePending = !0, this.#renderTopicVote(vote);
	      try {
	        await this.#onToggleTopicVote(vote.voted);
	      } catch (error) {
	        this.scope.destroyed || this.#onError(error);
	      } finally {
	        this.#topicVotePending = !1, this.scope.destroyed || this.#renderTopicVote(this.#controller.snapshot.vote);
	      }
	    }
	  }
	  #renderTopicVote(vote) {
	    if (!vote) {
	      this.#topicVote.replaceChildren(), this.#topicVote.hidden = !0;
	      return;
	    }
	    this.#topicVote.hidden = !1;
	    let button = this.#topicVote.querySelector(
	      ":scope > .ldp-topic-vote"
	    );
	    if (!button) {
	      button = this.#topicVote.ownerDocument.createElement("button"), button.type = "button", button.className = "ldp-topic-vote", button.dataset.topicVote = "";
	      const countNode = this.#topicVote.ownerDocument.createElement("span");
	      button.append("▲ ", countNode, " 票"), this.#topicVote.append(button);
	    }
	    button.classList.toggle("on", vote.voted), button.setAttribute("aria-pressed", String(vote.voted)), button.setAttribute(
	      "aria-label",
	      `${vote.voted ? "取消主题投票" : "为主题投票"},当前 ${vote.count} 票`
	    ), button.disabled = this.#topicVotePending || !vote.canVote && !vote.voted, button.toggleAttribute("aria-busy", this.#topicVotePending), button.querySelector("span").textContent = String(vote.count);
	  }
	  #queueTopicHints() {
	    this.scope.destroyed || this.#topicHintFrame || (this.#topicHintFrame = this.#requestFrame(() => {
	      if (this.#topicHintFrame = 0, this.scope.destroyed) return;
	      const row = this.#elements.topicIdentityHost, scroller = this.#topicScroller, hasOverflow = (!this.#topicTags.hidden || !this.#topicVote.hidden) && scroller.scrollWidth > row.clientWidth + 1, maxScrollLeft = Math.max(
	        0,
	        scroller.scrollWidth - scroller.clientWidth
	      );
	      row.classList.toggle("has-overflow", hasOverflow), row.classList.toggle(
	        "can-scroll-left",
	        hasOverflow && scroller.scrollLeft > 1
	      ), row.classList.toggle(
	        "can-scroll-right",
	        hasOverflow && scroller.scrollLeft < maxScrollLeft - 1
	      );
	    }));
	  }
	  #onTopicWheel(event) {
	    const rawDelta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
	    if (!rawDelta) return;
	    const maxScrollLeft = Math.max(
	      0,
	      this.#topicScroller.scrollWidth - this.#topicScroller.clientWidth
	    );
	    if (maxScrollLeft <= 1) return;
	    const delta = event.deltaMode === 1 ? rawDelta * 40 : event.deltaMode === 2 ? rawDelta * this.#topicScroller.clientWidth : rawDelta, nextScrollLeft = Math.min(
	      maxScrollLeft,
	      Math.max(0, this.#topicScroller.scrollLeft + delta)
	    );
	    nextScrollLeft !== this.#topicScroller.scrollLeft && (event.preventDefault(), event.stopPropagation(), this.#topicScroller.scrollLeft = nextScrollLeft, this.#queueTopicHints());
	  }
	  #renderOnlyOp(snapshot) {
	    const enabled = snapshot?.enabled === !0, available = snapshot?.available === !0;
	    this.#elements.onlyOpToggle.disabled = !available, this.#elements.onlyOpToggle.classList.toggle("active", enabled), this.#elements.onlyOpToggle.setAttribute(
	      "aria-pressed",
	      String(enabled)
	    ), this.#elements.onlyOpToggle.setAttribute(
	      "aria-label",
	      enabled ? "显示全部楼层" : "只看楼主"
	    );
	    const showProgress = !!(enabled && snapshot && !snapshot.complete && snapshot.totalPostCount > 0);
	    if (this.#elements.onlyOpProgress.hidden = !showProgress, !showProgress || !snapshot) {
	      this.#elements.onlyOpProgressValue.textContent = "", this.#elements.onlyOpProgress.style.removeProperty(
	        "--ldp-only-op-progress"
	      );
	      return;
	    }
	    const percent = Math.min(
	      100,
	      snapshot.loadedPostCount / snapshot.totalPostCount * 100
	    );
	    this.#elements.onlyOpProgress.style.setProperty(
	      "--ldp-only-op-progress",
	      `${percent.toFixed(1)}%`
	    ), this.#elements.onlyOpProgressValue.textContent = `已载入 ${snapshot.loadedPostCount}/${snapshot.totalPostCount} · 楼主 ${snapshot.ownerPostCount}`;
	  }
	}
}, "51f322842b7ef4890ced9a0218e8fdceaf3743c94d30c0f262ad7d007e9e42b0");

/* Source: lite/src/topic/reader-topic-local-archive-feature.ts */
runtime.register("src/topic/reader-topic-local-archive-feature.js", function(module, exports, require) {
	var reader_topic_local_archive_feature_exports = {};
	__export(reader_topic_local_archive_feature_exports, {
	  ReaderTopicLocalArchiveFeature: () => ReaderTopicLocalArchiveFeature
	});
	module.exports = __toCommonJS(reader_topic_local_archive_feature_exports);
	var import_html_element = require("../dom/html-element.js"), import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js");
	function archiveLabel(status) {
	  return status === 403 ? "已隐藏或无权访问" : `服务器返回 ${status}`;
	}
	function archivedPostLabel(status) {
	  return status === 403 ? "隐藏前正文" : `${status} 前正文`;
	}
	class ReaderTopicLocalArchiveFeature {
	  activationScope = "node";
	  scope;
	  #document;
	  #topicRoot;
	  #session;
	  #nowLabel;
	  #notice;
	  #roots = /* @__PURE__ */ new Map();
	  constructor(options) {
	    this.#document = options.document, this.#topicRoot = options.topicRoot, this.#session = options.session, this.#nowLabel = options.nowLabel ?? ((timestamp) => new Date(timestamp).toLocaleString()), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#notice = (0, import_html_element.htmlElement)(options.document, "aside", "ldp-topic-local-archive-notice"), this.#notice.setAttribute("role", "note"), this.#notice.hidden = !0, this.#topicRoot.prepend(this.#notice), this.scope.add(() => {
	      this.#roots.clear(), this.#notice.remove(), this.#topicRoot.classList.remove("is-local-archive-topic"), delete this.#topicRoot.dataset.localArchiveStatus;
	    }), this.#session.archiveChanges.subscribe(() => this.syncProjection(), this.scope), this.syncProjection();
	  }
	  afterRender(post, view) {
	    const postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber;
	    this.#roots.set(postNumber, view.slots.root), this.#projectPost(view.slots.root, postNumber);
	  }
	  attachRoot(root, postNumber) {
	    this.#roots.set(postNumber, root), this.#projectPost(root, postNumber);
	  }
	  detachRoot(root, postNumber) {
	    this.#roots.get(postNumber) === root && this.#roots.delete(postNumber);
	  }
	  syncProjection() {
	    const state = this.#session.localArchiveState(), topicUnavailable = state.topic;
	    this.#topicRoot.classList.toggle(
	      "is-local-archive-topic",
	      topicUnavailable !== null
	    ), topicUnavailable ? (this.#topicRoot.dataset.localArchiveStatus = String(topicUnavailable.status), this.#notice.hidden = !1, this.#notice.textContent = `本地存档 · ${archiveLabel(topicUnavailable.status)}。以下内容来自 ${this.#nowLabel(topicUnavailable.confirmedAt)} 前保留的正文,不会再按普通缓存期限自动清理,也不代表服务器当前版本。`) : (delete this.#topicRoot.dataset.localArchiveStatus, this.#notice.hidden = !0, this.#notice.textContent = "");
	    for (const [postNumber, root] of this.#roots)
	      this.#projectPost(root, postNumber, state);
	  }
	  #projectPost(root, postNumber, state = this.#session.localArchiveState()) {
	    const unavailable = state.posts.find((entry) => entry.postNumber === postNumber) ?? null;
	    root.classList.toggle("is-local-archive-post", unavailable !== null);
	    let note = root.querySelector(
	      ":scope > .ldp-post-body > .ldp-post-body-layer > .ldp-post-local-archive-note"
	    );
	    if (!unavailable) {
	      note?.remove(), delete root.dataset.localArchiveStatus;
	      return;
	    }
	    root.dataset.localArchiveStatus = String(unavailable.status), note || (note = (0, import_html_element.htmlElement)(
	      this.#document,
	      "aside",
	      "ldp-post-local-archive-note"
	    ), note.setAttribute("role", "note"), root.querySelector(
	      ":scope > .ldp-post-body > .ldp-post-body-layer"
	    )?.prepend(note)), note.textContent = `本地缓存 · ${archivedPostLabel(unavailable.status)} · ${this.#nowLabel(unavailable.confirmedAt)} 确认`, root.querySelector(":scope > .ldp-post-head .ldp-hidden-badge") && note.append((0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-post-local-archive-subtext",
	      "(已隐藏)"
	    ));
	  }
	}
}, "76b82c13cc71f53ac13e1cbb4b1cd8b9bf651f2c2e90549ef3575fa97e38f275");

/* Source: lite/src/topic/reader-topic-navigation-controller.ts */
runtime.register("src/topic/reader-topic-navigation-controller.js", function(module, exports, require) {
	var reader_topic_navigation_controller_exports = {};
	__export(reader_topic_navigation_controller_exports, {
	  ReaderTopicNavigationController: () => ReaderTopicNavigationController
	});
	module.exports = __toCommonJS(reader_topic_navigation_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_reply_ancestor_resolver = require("./reader-reply-ancestor-resolver.js");
	class ReaderTopicNavigationController {
	  scope;
	  changes = new import_signal.Signal();
	  #session;
	  #dom;
	  #hidden;
	  #onError;
	  #epoch = 0;
	  constructor(options) {
	    this.#session = options.session, this.#dom = options.dom, this.#hidden = options.hidden ?? null, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), options.listenUserScrollIntent && this.scope.add(options.listenUserScrollIntent(() => {
	      this.scope.destroyed || (this.#epoch += 1);
	    })), this.scope.add(() => {
	      this.#epoch += 1, this.changes.clear();
	    });
	  }
	  get revision() {
	    return this.#epoch;
	  }
	  isCurrent(revision) {
	    return !this.scope.destroyed && revision === this.#epoch;
	  }
	  async navigate(request) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: request.postNumber
	    }).postNumber, epoch = ++this.#epoch;
	    try {
	      if (request.cachedOnly !== !0 && (request.forceRefresh === !0 || !this.#session.postByNumber(postNumber)) && await this.#session.loadTarget(postNumber, {
	        scope: "around",
	        /* 远距目的性水合不能跳过尚未读取的顺序流中段。 */
	        advanceCursor: !1,
	        ...request.forceRefresh === !0 ? { forceRefresh: !0 } : {}
	      }), epoch !== this.#epoch || this.scope.destroyed)
	        return this.#result(request, postNumber, "superseded");
	      if (!this.#session.postByNumber(postNumber))
	        return this.#emit(this.#result(
	          request,
	          postNumber,
	          "unavailable"
	        ));
	      const ancestorResolution = request.cachedOnly === !0 ? null : await (0, import_reader_reply_ancestor_resolver.resolveReaderReplyAncestors)(
	        this.#session,
	        postNumber,
	        {
	          isActive: () => epoch === this.#epoch && !this.scope.destroyed
	        }
	      );
	      if (epoch !== this.#epoch || this.scope.destroyed)
	        return this.#result(request, postNumber, "superseded");
	      ancestorResolution?.error !== void 0 && this.#onError(ancestorResolution.error);
	      const revealOptions = {
	        source: request.source,
	        ...request.cachedOnly === !0 && request.revealAsFloor === !0 ? { degradedRootPostNumber: postNumber } : ancestorResolution && !ancestorResolution.complete ? {
	          degradedRootPostNumber: ancestorResolution.rootPostNumber
	        } : {},
	        ...request.revealAsFloor === !0 ? { revealAsFloor: !0 } : {},
	        ...request.alignment === void 0 ? {} : { alignment: request.alignment },
	        ...request.focus === void 0 ? {} : { focus: request.focus },
	        ...request.highlight === void 0 ? {} : { highlight: request.highlight }
	      };
	      this.#dom.prepareRevealPost?.(postNumber);
	      const reveal = request.revealAsFloor !== !0 && this.#hidden?.isHidden(postNumber) ? await this.#hidden.revealPost(postNumber, revealOptions) : this.#dom.revealPost(postNumber, revealOptions);
	      return epoch !== this.#epoch || this.scope.destroyed ? this.#result(request, postNumber, "superseded") : reveal ? this.#emit(Object.freeze({
	        postNumber,
	        source: request.source,
	        status: "revealed",
	        rootPostNumber: (0, import_identifiers.discoursePostReference)({
	          post_number: reveal.rootPostNumber
	        }).postNumber,
	        mounted: reveal.mounted,
	        element: reveal.element
	      })) : this.#emit(this.#result(
	        request,
	        postNumber,
	        "unresolved-tree"
	      ));
	    } catch (error) {
	      if (epoch !== this.#epoch || this.scope.destroyed)
	        return this.#result(request, postNumber, "superseded");
	      throw this.#onError(error), error;
	    }
	  }
	  cancel() {
	    this.#assertActive(), this.#epoch += 1;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #result(request, postNumber, status) {
	    return Object.freeze({
	      postNumber,
	      source: request.source,
	      status,
	      rootPostNumber: null,
	      mounted: !1
	    });
	  }
	  #emit(result) {
	    for (const error of this.changes.emit(result)) this.#onError(error);
	    return result;
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderTopicNavigationController 已销毁");
	  }
	}
}, "bda9dbdd4bf61753d4f769cdb94cf4e63e6b32548bfc17885f6dba793e94458c");

/* Source: lite/src/topic/reader-topic-navigation-preferences.ts */
runtime.register("src/topic/reader-topic-navigation-preferences.js", function(module, exports, require) {
	var reader_topic_navigation_preferences_exports = {};
	__export(reader_topic_navigation_preferences_exports, {
	  ReaderTopicNavigationPreferenceProjection: () => ReaderTopicNavigationPreferenceProjection
	});
	module.exports = __toCommonJS(reader_topic_navigation_preferences_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	const VARIABLES = Object.freeze([
	  "--ldp-jump-highlight-color",
	  "--ldp-jump-highlight-radius",
	  "--ldp-jump-highlight-border-width",
	  "--ldp-jump-highlight-duration",
	  "--ldp-jump-highlight-count"
	]), DEFAULTS = Object.freeze({
	  overscanScreens: 1.5,
	  maxMountedPostCount: 80,
	  highlightColor: "#0888cc",
	  highlightRadius: 10,
	  highlightBorderWidth: 1,
	  highlightRate: 0.8,
	  highlightCount: 2
	});
	function finiteRange(value, fallback, minimum, maximum) {
	  return Number.isFinite(value) ? Math.min(maximum, Math.max(minimum, value)) : fallback;
	}
	function normalize(preferences, performance) {
	  const overscanScreens = finiteRange(
	    performance?.streamOverscanScreens ?? DEFAULTS.overscanScreens,
	    DEFAULTS.overscanScreens,
	    0.25,
	    3
	  ), maxMountedPostCount = Math.round(finiteRange(
	    performance?.streamMaxMountedPostCount ?? DEFAULTS.maxMountedPostCount,
	    DEFAULTS.maxMountedPostCount,
	    24,
	    128
	  )), highlightColor = /^#[0-9a-f]{6}$/i.test(preferences.jumpHighlightColor) ? preferences.jumpHighlightColor.toLowerCase() : DEFAULTS.highlightColor, highlightRadius = Math.round(finiteRange(
	    preferences.jumpHighlightRadius,
	    DEFAULTS.highlightRadius,
	    0,
	    24
	  )), highlightBorderWidth = Math.round(finiteRange(
	    preferences.jumpHighlightBorderWidth,
	    DEFAULTS.highlightBorderWidth,
	    0,
	    4
	  )), highlightRate = Math.round(
	    finiteRange(
	      preferences.jumpHighlightRate,
	      DEFAULTS.highlightRate,
	      0.5,
	      2
	    ) * 10
	  ) / 10, highlightCount = Math.round(finiteRange(
	    preferences.jumpHighlightCount,
	    DEFAULTS.highlightCount,
	    1,
	    6
	  )), highlightStepDurationMs = Math.round(1e3 / highlightRate);
	  return Object.freeze({
	    overscanScreens,
	    maxMountedPostCount,
	    highlightColor,
	    highlightRadius,
	    highlightBorderWidth,
	    highlightRate,
	    highlightCount,
	    highlightStepDurationMs,
	    highlightLifetimeMs: highlightStepDurationMs * highlightCount
	  });
	}
	function snapshotsEqual(left, right) {
	  return left.overscanScreens === right.overscanScreens && left.maxMountedPostCount === right.maxMountedPostCount && left.highlightColor === right.highlightColor && left.highlightRadius === right.highlightRadius && left.highlightBorderWidth === right.highlightBorderWidth && left.highlightRate === right.highlightRate && left.highlightCount === right.highlightCount && left.highlightStepDurationMs === right.highlightStepDurationMs && left.highlightLifetimeMs === right.highlightLifetimeMs;
	}
	class ReaderTopicNavigationPreferenceProjection {
	  scope;
	  #root;
	  #readPerformance;
	  #previous = /* @__PURE__ */ new Map();
	  #preferences;
	  #preview = null;
	  #snapshot;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#root = options.root, this.#readPerformance = options.readPerformance ?? (() => null), this.#preferences = options.preferences;
	    for (const name of VARIABLES) {
	      const readPriority = this.#root.style.getPropertyPriority;
	      this.#previous.set(name, Object.freeze({
	        value: this.#root.style.getPropertyValue(name),
	        priority: typeof readPriority == "function" ? readPriority.call(this.#root.style, name) : ""
	      }));
	    }
	    this.#snapshot = normalize(
	      options.preferences,
	      this.#readPerformance()
	    ), this.#applyCss(), this.scope.add(() => this.#restoreCss());
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  apply(preferences) {
	    this.scope.destroyed || (this.#preferences = preferences, this.#commit(normalize(
	      this.#preview ?? this.#preferences,
	      this.#readPerformance()
	    )));
	  }
	  preview(preferences) {
	    if (this.scope.destroyed) return;
	    const performance = this.#readPerformance(), snapshot = normalize(preferences, performance);
	    this.#preview = snapshotsEqual(
	      snapshot,
	      normalize(this.#preferences, performance)
	    ) ? null : preferences, this.#commit(snapshot);
	  }
	  clearPreview() {
	    this.scope.destroyed || !this.#preview || (this.#preview = null, this.#commit(normalize(
	      this.#preferences,
	      this.#readPerformance()
	    )));
	  }
	  refreshPerformance() {
	    if (this.scope.destroyed) return;
	    const performance = this.#readPerformance();
	    this.#commit(Object.freeze({
	      ...this.#snapshot,
	      overscanScreens: finiteRange(
	        performance?.streamOverscanScreens ?? DEFAULTS.overscanScreens,
	        DEFAULTS.overscanScreens,
	        0.25,
	        3
	      ),
	      maxMountedPostCount: Math.round(finiteRange(
	        performance?.streamMaxMountedPostCount ?? DEFAULTS.maxMountedPostCount,
	        DEFAULTS.maxMountedPostCount,
	        24,
	        128
	      ))
	    }));
	  }
	  readOverscan() {
	    const overscanScreens = this.#snapshot.overscanScreens;
	    return Object.freeze({
	      beforeScreens: overscanScreens,
	      afterScreens: overscanScreens
	    });
	  }
	  readHighlightLifetimeMs() {
	    return this.#snapshot.highlightLifetimeMs;
	  }
	  readMaxMountedPostCount() {
	    return this.#snapshot.maxMountedPostCount;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #commit(snapshot) {
	    snapshotsEqual(snapshot, this.#snapshot) || (this.#snapshot = snapshot, this.#applyCss());
	  }
	  #applyCss() {
	    const snapshot = this.#snapshot;
	    this.#root.style.setProperty(
	      "--ldp-jump-highlight-color",
	      snapshot.highlightColor
	    ), this.#root.style.setProperty(
	      "--ldp-jump-highlight-radius",
	      `${snapshot.highlightRadius}px`
	    ), this.#root.style.setProperty(
	      "--ldp-jump-highlight-border-width",
	      `${snapshot.highlightBorderWidth}px`
	    ), this.#root.style.setProperty(
	      "--ldp-jump-highlight-duration",
	      `${snapshot.highlightStepDurationMs}ms`
	    ), this.#root.style.setProperty(
	      "--ldp-jump-highlight-count",
	      String(snapshot.highlightCount)
	    );
	  }
	  #restoreCss() {
	    for (const name of VARIABLES) {
	      const previous = this.#previous.get(name);
	      previous?.value ? this.#root.style.setProperty(
	        name,
	        previous.value,
	        previous.priority
	      ) : this.#root.style.removeProperty(name);
	    }
	  }
	}
}, "021614753d8d9c7366451df05ef36e606d15b2101d07f9b3bf45efa4ec826017");

/* Source: lite/src/topic/reader-topic-only-op-controller.ts */
runtime.register("src/topic/reader-topic-only-op-controller.js", function(module, exports, require) {
	var reader_topic_only_op_controller_exports = {};
	__export(reader_topic_only_op_controller_exports, {
	  ReaderTopicOnlyOpController: () => ReaderTopicOnlyOpController
	});
	module.exports = __toCommonJS(reader_topic_only_op_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_value_record = require("../kernel/value-record.js"), import_reader_topic_header = require("./reader-topic-header.js");
	function text(value) {
	  return String(value ?? "").trim();
	}
	function positiveCount(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : 0;
	}
	class ReaderTopicOnlyOpController {
	  scope;
	  changes = new import_signal.Signal();
	  #session;
	  #presentation;
	  #onProjectionChanged;
	  #onEnabledChanged;
	  #onError;
	  #enabled = !1;
	  #snapshot;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#session = options.session, this.#presentation = options.presentation, this.#onProjectionChanged = options.onProjectionChanged, this.#onEnabledChanged = options.onEnabledChanged ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.#snapshot = this.#readSnapshot(), (options.presentationChanges ?? this.#session.changes).subscribe((commit) => {
	      this.refresh(commit.changedPostNumbers.length > 0);
	    }, this.scope), this.scope.add(() => {
	      this.#enabled && this.#notifyEnabledChanged(!1), this.#presentation.setPostFilter(null), this.changes.clear();
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  setEnabled(enabled) {
	    const next = enabled === !0 && this.#snapshot.available;
	    return next === this.#enabled ? !1 : (this.#enabled = next, this.#notifyEnabledChanged(next), this.#applyProjection(), this.refresh(), !0);
	  }
	  toggle() {
	    return this.setEnabled(!this.#enabled);
	  }
	  refresh(invalidateFilter = !1) {
	    const next = this.#readSnapshot();
	    if (this.#enabled && !next.available)
	      return this.#enabled = !1, this.#notifyEnabledChanged(!1), this.#applyProjection(), this.refresh();
	    if (this.#enabled && next.ownerUsername !== this.#snapshot.ownerUsername && this.#applyProjection(next.ownerUsername), invalidateFilter && this.#enabled && this.#presentation.invalidatePostFilter() && this.#notifyProjectionChanged(!1), next.enabled === this.#snapshot.enabled && next.available === this.#snapshot.available && next.ownerUsername === this.#snapshot.ownerUsername && next.loadedPostCount === this.#snapshot.loadedPostCount && next.totalPostCount === this.#snapshot.totalPostCount && next.ownerPostCount === this.#snapshot.ownerPostCount && next.complete === this.#snapshot.complete) return this.#snapshot;
	    this.#snapshot = next;
	    for (const error of this.changes.emit(next)) this.#onError(error);
	    return next;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #applyProjection(owner = this.#snapshot.ownerUsername) {
	    this.#presentation.setPostFilter(
	      this.#enabled && owner ? Object.freeze({
	        key: `only-op:${owner}`,
	        hideDescendantMatches: !0,
	        ancestorBoundaryPostNumber: 1,
	        matches: (postNumber) => text(this.#session.postByNumber(postNumber)?.username) === owner
	      }) : null
	    ) && this.#notifyProjectionChanged(!0);
	  }
	  #notifyProjectionChanged(resetScroll) {
	    try {
	      this.#onProjectionChanged(resetScroll);
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  #notifyEnabledChanged(enabled) {
	    try {
	      this.#onEnabledChanged(enabled);
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  #readSnapshot() {
	    const posts = this.#session.cachedPosts(), owner = (0, import_reader_topic_header.readerTopicOwnerUsername)(this.#session.topic, posts), topic = (0, import_value_record.valueRecord)(this.#session.topic), totalPostCount = Math.max(
	      positiveCount(topic?.highest_post_number),
	      positiveCount(topic?.posts_count),
	      posts.reduce(
	        (maximum, post) => Math.max(maximum, positiveCount(post.post_number)),
	        0
	      )
	    );
	    return Object.freeze({
	      enabled: this.#enabled,
	      available: !!owner,
	      ownerUsername: owner,
	      loadedPostCount: posts.length,
	      totalPostCount,
	      ownerPostCount: owner ? posts.filter((post) => text(post.username) === owner).length : 0,
	      complete: this.#session.loadDone === !0 || totalPostCount > 0 && posts.length >= totalPostCount
	    });
	  }
	}
}, "26786da74701ab1ec96b5181a0950b1463e676432c25bc31fdd17eb8d88e0bbd");

/* Source: lite/src/topic/reader-topic-scroll-adapter.ts */
runtime.register("src/topic/reader-topic-scroll-adapter.js", function(module, exports, require) {
	var reader_topic_scroll_adapter_exports = {};
	__export(reader_topic_scroll_adapter_exports, {
	  ReaderBoostTargetHighlightController: () => ReaderBoostTargetHighlightController,
	  ReaderTopicJumpHighlightController: () => ReaderTopicJumpHighlightController,
	  ReaderTopicScrollAdapter: () => ReaderTopicScrollAdapter
	});
	module.exports = __toCommonJS(reader_topic_scroll_adapter_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_event_target = require("../dom/event-target.js");
	function finiteNonNegative(value, fallback = 0) {
	  return Number.isFinite(value) && value >= 0 ? value : fallback;
	}
	function wheelBlockDelta(event, scrollRoot) {
	  return event.deltaMode === 1 ? event.deltaY * 40 : event.deltaMode === 2 ? event.deltaY * scrollRoot.clientHeight : event.deltaY;
	}
	function nestedScrollTargetCanConsume(event, scrollRoot, delta) {
	  const target = (0, import_event_target.eventElement)(event);
	  if (!target || !scrollRoot.contains(target)) return !1;
	  let candidate = target;
	  for (; candidate && candidate !== scrollRoot; ) {
	    const maxScrollTop = candidate.scrollHeight - candidate.clientHeight, view = candidate.ownerDocument.defaultView, overflowY = (typeof view?.getComputedStyle == "function" ? view.getComputedStyle(candidate).overflowY : "") || candidate.style.overflowY;
	    if (maxScrollTop > 1 && /(auto|scroll|overlay)/.test(overflowY) && (delta < 0 ? candidate.scrollTop > 0 : candidate.scrollTop < maxScrollTop - 1)) return !0;
	    candidate = candidate.parentElement;
	  }
	  return !1;
	}
	function defaultResizeObserverFactory(callback) {
	  return typeof ResizeObserver != "function" ? null : new ResizeObserver((entries) => callback(entries.map((entry) => {
	    const borderBox = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize;
	    return Object.freeze({
	      target: entry.target,
	      blockSize: borderBox?.blockSize ?? entry.contentRect.height
	    });
	  })));
	}
	class ReaderTopicJumpHighlightController {
	  scope;
	  #readLifetimeMs;
	  #prefersReducedMotion;
	  #createResizeObserver;
	  #schedule;
	  #cancel;
	  #active = null;
	  #token = 0;
	  constructor(options = {}) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#readLifetimeMs = options.readLifetimeMs ?? (() => 2500), this.#prefersReducedMotion = options.prefersReducedMotion ?? (() => !1), this.#createResizeObserver = options.createResizeObserver ?? defaultResizeObserverFactory, this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.scope.add(() => this.clear());
	  }
	  highlight(target) {
	    if (this.scope.destroyed || !target.isConnected) return;
	    this.clear();
	    const token = String(++this.#token);
	    target.classList.remove("ldp-jump-highlight"), target.style.removeProperty("--ldp-first-post-jump-highlight-height");
	    const anchor = this.#syncFirstPostRegion(target);
	    target.offsetWidth, target.dataset.jumpHighlightToken = token, target.classList.add("ldp-jump-highlight");
	    const observer = anchor ? this.#createResizeObserver(() => {
	      target.isConnected && target.dataset.jumpHighlightToken === token && this.#syncFirstPostRegion(target);
	    }) : null;
	    observer?.observe(target), anchor && observer?.observe(anchor);
	    const lifetime = this.#prefersReducedMotion() ? 1e3 : Math.max(1, finiteNonNegative(this.#readLifetimeMs(), 2500)), timer = this.#schedule(() => {
	      target.dataset.jumpHighlightToken === token && this.#clearTarget(target), observer?.disconnect(), this.#active?.token === token && (this.#active = null);
	    }, lifetime);
	    this.#active = Object.freeze({ target, token, timer, observer });
	  }
	  clear() {
	    const active = this.#active;
	    active && (this.#active = null, this.#cancel(active.timer), active.observer?.disconnect(), active.target.dataset.jumpHighlightToken === active.token && this.#clearTarget(active.target));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #syncFirstPostRegion(target) {
	    if (target.dataset.postNumber !== "1") return null;
	    const actions = target.querySelector(
	      ":scope > .ldp-post-body .ldp-reactions, :scope > .ldp-reactions"
	    ), targetRect = target.getBoundingClientRect();
	    if (actions) {
	      const actionsRect = actions.getBoundingClientRect();
	      if (actionsRect.height > 0 && actionsRect.bottom > targetRect.top)
	        return this.#setFirstPostRegion(target, targetRect, actionsRect), actions;
	    }
	    const body = target.querySelector(
	      ":scope > .ldp-post-body"
	    );
	    return body ? (this.#setFirstPostRegion(
	      target,
	      targetRect,
	      body.getBoundingClientRect()
	    ), body) : null;
	  }
	  #setFirstPostRegion(target, targetRect, boundaryRect) {
	    const height = Math.min(
	      targetRect.height,
	      Math.max(0, boundaryRect.bottom - targetRect.top)
	    );
	    target.style.setProperty(
	      "--ldp-first-post-jump-highlight-height",
	      `${Math.ceil(height)}px`
	    );
	  }
	  #clearTarget(target) {
	    target.classList.remove("ldp-jump-highlight"), target.style.removeProperty("--ldp-first-post-jump-highlight-height"), delete target.dataset.jumpHighlightToken;
	  }
	}
	class ReaderBoostTargetHighlightController {
	  scope;
	  #readLifetimeMs;
	  #prefersReducedMotion;
	  #schedule;
	  #cancel;
	  #active = null;
	  #token = 0;
	  constructor(options = {}) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#readLifetimeMs = options.readLifetimeMs ?? (() => 2500), this.#prefersReducedMotion = options.prefersReducedMotion ?? (() => !1), this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.scope.add(() => this.clear());
	  }
	  highlight(target) {
	    if (this.scope.destroyed || !target.isConnected) return;
	    this.clear();
	    const token = String(++this.#token);
	    target.classList.remove("ldp-boost-target-highlight"), target.offsetWidth, target.dataset.boostTargetHighlightToken = token, target.classList.add("ldp-boost-target-highlight");
	    const lifetime = this.#prefersReducedMotion() ? 1e3 : Math.max(1, finiteNonNegative(this.#readLifetimeMs(), 2500)), timer = this.#schedule(() => {
	      target.dataset.boostTargetHighlightToken === token && this.#clearTarget(target), this.#active?.token === token && (this.#active = null);
	    }, lifetime);
	    this.#active = Object.freeze({ target, token, timer });
	  }
	  clear() {
	    const active = this.#active;
	    active && (this.#active = null, this.#cancel(active.timer), active.target.dataset.boostTargetHighlightToken === active.token && this.#clearTarget(active.target));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #clearTarget(target) {
	    target.classList.remove("ldp-boost-target-highlight"), delete target.dataset.boostTargetHighlightToken;
	  }
	}
	const SCROLLING_KEYS = /* @__PURE__ */ new Set([
	  "ArrowDown",
	  "ArrowUp",
	  "End",
	  "Home",
	  "PageDown",
	  "PageUp",
	  " "
	]), USER_SCROLL_SESSION_GAP_MS = 500, STATIONARY_LOCK_FALLBACK_FRAMES = 3, STATIONARY_LOCK_AFTER_COMMIT_FRAMES = 2;
	function isEditableScrollTarget(target) {
	  const candidate = target;
	  return !candidate || typeof candidate.closest != "function" ? !1 : !!candidate.closest(
	    'input, textarea, select, [contenteditable=""], [contenteditable="true"]'
	  );
	}
	class ReaderTopicScrollAdapter {
	  scope;
	  highlight;
	  #scrollRoot;
	  #readOverscan;
	  #readMaxMountedPostCount;
	  #readTopInset;
	  #requestFrame;
	  #cancelFrame;
	  #now;
	  #observesViewportSize;
	  #windowChangeListeners = /* @__PURE__ */ new Set();
	  #userScrollIntentListeners = /* @__PURE__ */ new Set();
	  #directUserScrollIntentListeners = /* @__PURE__ */ new Set();
	  #viewportSize = 1;
	  #viewportSizeDirty = !1;
	  #scrollOffset = 0;
	  #pendingScrollOffset = null;
	  #scrollOffsetDirty = !1;
	  #scrollFrame = 0;
	  #lastUserScrollAt = 0;
	  #lastUserScrollDirection = 0;
	  #userScrollSessionActive = !1;
	  #lastTouchClientY = null;
	  #stationaryAnchor = null;
	  #stationaryLockPending = !1;
	  #stationaryLockFrame = 0;
	  #stationaryLockSettleFrames = 0;
	  #viewportMutationAnchor = null;
	  #viewportMutationToken = 0;
	  #internalScrollWriteOffset = null;
	  #programmaticScrollTransactionDepth = 0;
	  #stationaryMutationObserver = null;
	  #stationaryResizeObserver = null;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#scrollRoot = options.scrollRoot, this.#readOverscan = options.readOverscan ?? (() => ({})), this.#readMaxMountedPostCount = options.readMaxMountedPostCount ?? (() => {
	    }), this.#readTopInset = options.readTopInset ?? (() => 0), this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.#now = options.now ?? (() => performance.now()), this.#scrollRoot.classList.remove("ldp-stream-viewport-anchor");
	    const createMutationObserver = options.createMutationObserver ?? ((callback) => {
	      const NativeMutationObserver = this.#scrollRoot.ownerDocument.defaultView?.MutationObserver;
	      return NativeMutationObserver ? new NativeMutationObserver(callback) : null;
	    });
	    this.#stationaryMutationObserver = createMutationObserver(() => {
	      this.#observeStationaryContentSize(), this.#restoreStationaryViewport();
	    });
	    const NativeResizeObserver = this.#scrollRoot.ownerDocument.defaultView?.ResizeObserver;
	    NativeResizeObserver && (this.#stationaryResizeObserver = new NativeResizeObserver(() => {
	      this.#restoreStationaryViewport();
	    })), this.scope.add(() => this.#releaseStationaryViewport()), this.scope.add(() => this.#cancelPendingStationaryViewportLock()), this.scope.add(() => this.#cancelViewportMutation(void 0, !1));
	    const createResizeObserver = options.createResizeObserver ?? defaultResizeObserverFactory, viewportObserver = createResizeObserver((entries) => {
	      const entry = entries.find(({ target }) => target === this.#scrollRoot), viewportSize = Math.max(
	        1,
	        finiteNonNegative(entry?.blockSize ?? 0) || this.#measureViewportSize()
	      ), scrollOffset = finiteNonNegative(this.#scrollRoot.scrollTop);
	      viewportSize === this.#viewportSize && scrollOffset === this.#scrollOffset || (this.#viewportSize = viewportSize, this.#scrollOffset = scrollOffset, this.#pendingScrollOffset = scrollOffset, this.#scrollOffsetDirty = !1, this.#notifyWindowChange());
	    });
	    this.#observesViewportSize = viewportObserver !== null, viewportObserver || (this.#viewportSize = this.#measureViewportSize(), this.#scrollOffset = finiteNonNegative(this.#scrollRoot.scrollTop)), viewportObserver?.observe(this.#scrollRoot), viewportObserver && this.scope.add(() => viewportObserver.disconnect()), options.viewportChangeTarget && this.scope.listen(
	      options.viewportChangeTarget,
	      "ldp-reader-window-change",
	      () => {
	        this.#viewportSizeDirty = !0, this.#notifyWindowChange();
	      }
	    ), this.highlight = new ReaderTopicJumpHighlightController({
	      ...options.readLifetimeMs ? { readLifetimeMs: options.readLifetimeMs } : {},
	      ...options.prefersReducedMotion ? { prefersReducedMotion: options.prefersReducedMotion } : {},
	      createResizeObserver,
	      ...options.schedule ? { schedule: options.schedule } : {},
	      ...options.cancel ? { cancel: options.cancel } : {},
	      parentScope: this.scope
	    }), this.scope.listen(
	      this.#scrollRoot,
	      "scroll",
	      () => {
	        this.#claimScrollOnlyUserInput() && this.#markUserScrollProgress(), this.#scrollOffsetDirty = !0, this.#scheduleScrollCommit();
	      },
	      { passive: !0 }
	    ), this.scope.listen(this.#scrollRoot, "scrollend", () => {
	      this.#finishUserScrollSession();
	    }, { passive: !0 }), this.scope.listen(this.#scrollRoot, "wheel", (event) => {
	      const wheel = event;
	      if (wheel.ctrlKey) return;
	      const delta = wheelBlockDelta(wheel, this.#scrollRoot), direction = this.#directionOf(delta);
	      if (direction === 0) {
	        this.#markUserScrollIntent();
	        return;
	      }
	      if (direction < 0 && !nestedScrollTargetCanConsume(wheel, this.#scrollRoot, delta)) {
	        wheel.preventDefault(), this.#markUserScrollIntent(direction);
	        const requestedOffset = Math.max(
	          0,
	          finiteNonNegative(this.#scrollRoot.scrollTop) + delta
	        ), actualOffset = this.#writeScrollRootOffset(requestedOffset);
	        this.#scrollOffset = actualOffset, this.#pendingScrollOffset = actualOffset, this.#scrollOffsetDirty = !1, this.#notifyWindowChange();
	        return;
	      }
	      this.#markUserScrollIntent(direction);
	    }, { passive: !1 }), this.scope.listen(this.#scrollRoot, "touchstart", (event) => {
	      this.#lastTouchClientY = event.touches?.[0]?.clientY ?? null, this.#markUserScrollIntent();
	    }, { passive: !0 }), this.scope.listen(this.#scrollRoot, "touchmove", (event) => {
	      const clientY = event.touches?.[0]?.clientY, direction = clientY === void 0 || this.#lastTouchClientY === null ? 0 : this.#directionOf(this.#lastTouchClientY - clientY);
	      this.#lastTouchClientY = clientY ?? null, this.#markUserScrollIntent(direction);
	    }, { passive: !0 });
	    for (const type of ["touchend", "touchcancel"])
	      this.scope.listen(this.#scrollRoot, type, () => {
	        this.#lastTouchClientY = null;
	      }, { passive: !0 });
	    this.scope.listen(this.#scrollRoot, "keydown", (event) => {
	      const keyboard = event;
	      keyboard.defaultPrevented || keyboard.altKey || keyboard.ctrlKey || keyboard.metaKey || !SCROLLING_KEYS.has(keyboard.key) || isEditableScrollTarget(keyboard.target) || this.#markUserScrollIntent(this.#keyboardDirection(keyboard));
	    }), this.scope.add(() => {
	      this.#scrollFrame && (this.#cancelFrame(this.#scrollFrame), this.#scrollFrame = 0, this.#pendingScrollOffset = null, this.#scrollOffsetDirty = !1);
	    }), this.scope.add(() => this.#windowChangeListeners.clear()), this.scope.add(() => this.#userScrollIntentListeners.clear()), this.scope.add(() => this.#directUserScrollIntentListeners.clear());
	  }
	  readWindowInput() {
	    const overscan = this.#readOverscan(), maxMountedPostCount = this.#readMaxMountedPostCount(), scrollOffset = this.#refreshPendingScrollOffset(), bootstrapViewportSize = this.#observesViewportSize && this.#viewportSize <= 1, viewportSize = !this.#observesViewportSize || this.#viewportSizeDirty || bootstrapViewportSize ? this.#measureViewportSize() : this.#viewportSize;
	    (this.#viewportSizeDirty || bootstrapViewportSize) && (this.#viewportSize = viewportSize, this.#viewportSizeDirty = !1);
	    const preservedPostNumber = this.#stationaryAnchor?.postNumber ?? this.#viewportMutationAnchor?.postNumber;
	    return Object.freeze({
	      scrollOffset,
	      viewportSize,
	      ...preservedPostNumber === void 0 ? {} : { preservePostNumber: preservedPostNumber },
	      /*
	       * overscan 是用户/性能策略的稳定窗口契约。滚动方向只改变 offset;
	       * 若在反向瞬间翻转前后边界,会额外整批卸载/挂载树节点并制造尖峰。
	       */
	      overscanBeforeScreens: finiteNonNegative(
	        Number(overscan.beforeScreens),
	        1
	      ),
	      overscanAfterScreens: finiteNonNegative(
	        Number(overscan.afterScreens),
	        1
	      ),
	      ...maxMountedPostCount === void 0 ? {} : { maxMountedPostCount }
	    });
	  }
	  lastUserScrollAt() {
	    return this.#lastUserScrollAt;
	  }
	  lastUserScrollDirection() {
	    return this.#lastUserScrollDirection;
	  }
	  remainingUserIdleMs(minimumIdleMs) {
	    const idleMs = finiteNonNegative(Number(minimumIdleMs));
	    return this.#lastUserScrollAt <= 0 ? 0 : Math.max(0, idleMs - (this.#now() - this.#lastUserScrollAt));
	  }
	  readVisibleViewportAnchor(elements) {
	    const visible = this.#findTopVisiblePost(elements);
	    return visible ? Object.freeze({
	      postNumber: visible.postNumber,
	      postOffset: visible.ownerOffset,
	      scrollTop: finiteNonNegative(this.#scrollRoot.scrollTop)
	    }) : null;
	  }
	  beginViewportMutation(elements) {
	    if (this.#stationaryAnchor || this.#viewportMutationAnchor) return null;
	    const visible = this.#findTopVisiblePost(elements);
	    if (!visible) return null;
	    const token = ++this.#viewportMutationToken;
	    return this.#viewportMutationAnchor = Object.freeze({
	      token,
	      postNumber: visible.postNumber,
	      markerRole: visible.marker === visible.owner ? "owner" : "header",
	      markerOffset: visible.markerOffset,
	      ownerOffset: visible.ownerOffset,
	      scrollTop: finiteNonNegative(this.#scrollRoot.scrollTop)
	    }), this.#syncViewportAnchorClass(), Object.freeze({
	      restore: () => this.#restoreViewportMutation(token),
	      cancel: () => this.#cancelViewportMutation(token, !1)
	    });
	  }
	  /** 虚拟窗口及其同步投影已经提交;停稳锁可以开始等待干净的布局边界。 */
	  notifyVirtualWindowCommit() {
	    !this.#stationaryLockPending || this.#userScrollSessionActive || this.#viewportMutationAnchor || (this.#stationaryLockSettleFrames = STATIONARY_LOCK_AFTER_COMMIT_FRAMES, this.#stationaryLockFrame && (this.#cancelFrame(this.#stationaryLockFrame), this.#stationaryLockFrame = 0), this.#scheduleStationaryViewportLock());
	  }
	  #measureViewportSize() {
	    const clientHeight = finiteNonNegative(this.#scrollRoot.clientHeight), rectHeight = clientHeight > 0 ? 0 : finiteNonNegative(
	      this.#scrollRoot.getBoundingClientRect().height
	    );
	    return Math.max(1, clientHeight || rectHeight || 1);
	  }
	  applyScrollCompensation(delta) {
	    if (!Number.isFinite(delta) || delta === 0) return;
	    this.#refreshPendingScrollOffset();
	    const requestedOffset = Math.max(
	      0,
	      (this.#pendingScrollOffset ?? this.#scrollOffset) + delta
	    ), actualOffset = this.#writeScrollRootOffset(requestedOffset);
	    this.#scrollOffset = actualOffset, this.#pendingScrollOffset !== null && (this.#pendingScrollOffset = actualOffset), this.#restoreStationaryViewport();
	  }
	  listenScroll(listener) {
	    return this.scope.destroyed ? () => {
	    } : (this.#windowChangeListeners.add(listener), this.scope.add(() => {
	      this.#windowChangeListeners.delete(listener);
	    }));
	  }
	  listenUserScrollIntent(listener) {
	    return this.scope.destroyed ? () => {
	    } : (this.#userScrollIntentListeners.add(listener), this.scope.add(() => {
	      this.#userScrollIntentListeners.delete(listener);
	    }));
	  }
	  listenDirectUserScrollIntent(listener) {
	    return this.scope.destroyed ? () => {
	    } : (this.#directUserScrollIntentListeners.add(listener), this.scope.add(() => {
	      this.#directUserScrollIntentListeners.delete(listener);
	    }));
	  }
	  writeScrollOffset(offset) {
	    const normalized = finiteNonNegative(offset), maxOffset = Math.max(
	      0,
	      finiteNonNegative(this.#scrollRoot.scrollHeight) - finiteNonNegative(this.#scrollRoot.clientHeight)
	    );
	    this.#setScrollOffset(maxOffset > 0 ? Math.min(normalized, maxOffset) : normalized);
	  }
	  readScrollRange() {
	    return Math.max(
	      0,
	      finiteNonNegative(this.#scrollRoot.scrollHeight) - finiteNonNegative(this.#scrollRoot.clientHeight)
	    );
	  }
	  withProgrammaticScrollTransaction(commit) {
	    if (this.scope.destroyed) return;
	    const outermost = this.#programmaticScrollTransactionDepth === 0;
	    this.#programmaticScrollTransactionDepth += 1, outermost && (this.#cancelViewportMutation(void 0, !1), this.#releaseStationaryViewport(), this.#scrollRoot.classList.add("ldp-stream-programmatic-scroll"));
	    try {
	      if (commit(), outermost) {
	        this.#scrollRoot.getBoundingClientRect();
	        const actualOffset = finiteNonNegative(this.#scrollRoot.scrollTop);
	        this.#scrollOffset = actualOffset, this.#pendingScrollOffset = actualOffset, this.#scrollOffsetDirty = !1;
	      }
	    } finally {
	      this.#programmaticScrollTransactionDepth = Math.max(
	        0,
	        this.#programmaticScrollTransactionDepth - 1
	      ), outermost && this.#scrollRoot.classList.remove("ldp-stream-programmatic-scroll");
	    }
	  }
	  alignPost(target, options) {
	    if (!target.isConnected) return;
	    const rootRect = this.#scrollRoot.getBoundingClientRect(), targetRect = target.getBoundingClientRect(), topInset = Math.min(
	      rootRect.height,
	      finiteNonNegative(this.#readTopInset())
	    ), configuredVisibleTop = rootRect.top + topInset, frozenHeaderBottom = options.viewportOffset === void 0 ? configuredVisibleTop : this.#scrollRoot.closest(".ldp-modal")?.querySelector(":scope > .ldp-header")?.getBoundingClientRect().bottom ?? configuredVisibleTop, visibleTop = Math.min(
	      rootRect.bottom,
	      Math.max(configuredVisibleTop, frozenHeaderBottom) + finiteNonNegative(options.viewportOffset ?? 0)
	    ), visibleBottom = rootRect.bottom, visibleHeight = Math.max(1, visibleBottom - visibleTop), fits = targetRect.height <= visibleHeight, alignment = options.alignment ?? (target.dataset.postNumber === "1" ? "start" : "center");
	    let correction = 0;
	    alignment === "nearest" ? targetRect.top < visibleTop && targetRect.bottom > visibleBottom ? correction = 0 : targetRect.top < visibleTop ? correction = targetRect.top - visibleTop : targetRect.bottom > visibleBottom && (correction = targetRect.bottom - visibleBottom) : alignment === "center" && fits ? correction = targetRect.top + targetRect.height / 2 - (visibleTop + visibleHeight / 2) : correction = targetRect.top - visibleTop, Math.abs(correction) >= 1 && this.writeScrollOffset(this.#scrollOffset + correction), options.focus && this.#focus(target), options.highlight !== !1 && this.highlight.highlight(target);
	  }
	  highlightPost(target) {
	    target.isConnected && this.highlight.highlight(target);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #setScrollOffset(offset) {
	    this.#cancelPendingStationaryViewportLock(), this.#cancelViewportMutation(void 0, !1), this.#releaseStationaryViewport();
	    const requestedOffset = Math.max(0, finiteNonNegative(offset));
	    this.#scrollOffset = requestedOffset, this.#pendingScrollOffset = requestedOffset, this.#scrollOffsetDirty = !1, this.#lastUserScrollAt = 0, this.#lastUserScrollDirection = 0, this.#userScrollSessionActive = !1;
	    const actualOffset = this.#writeScrollRootOffset(requestedOffset);
	    this.#scrollOffset = actualOffset, this.#pendingScrollOffset = actualOffset;
	  }
	  #focus(target) {
	    const previousTabIndex = target.getAttribute("tabindex");
	    previousTabIndex === null && target.setAttribute("tabindex", "-1");
	    try {
	      target.focus({ preventScroll: !0 });
	    } catch {
	      target.focus();
	    }
	    previousTabIndex === null ? target.removeAttribute("tabindex") : target.setAttribute("tabindex", previousTabIndex);
	  }
	  #findTopVisiblePost(elements) {
	    const rootRect = this.#scrollRoot.getBoundingClientRect(), visibleTop = rootRect.top + Math.min(
	      rootRect.height,
	      finiteNonNegative(this.#readTopInset())
	    ), visibleBottom = rootRect.bottom, candidates = elements.flatMap((owner) => {
	      const postNumber = Number(owner.dataset.postNumber);
	      if (!Number.isSafeInteger(postNumber) || postNumber <= 0 || !owner.isConnected || !this.#scrollRoot.contains(owner) || owner.hidden) return [];
	      const marker2 = owner.querySelector(":scope > .ldp-post-head") ?? owner, markerRect = marker2.getBoundingClientRect(), ownerRect = owner.getBoundingClientRect();
	      return [{ postNumber, marker: marker2, markerRect, owner, ownerRect }];
	    }), visibleHeaders = candidates.filter(
	      ({ markerRect }) => markerRect.bottom > visibleTop + 1 && markerRect.top < visibleBottom - 1
	    ).sort(
	      (left, right) => Math.max(0, left.markerRect.top - visibleTop) - Math.max(0, right.markerRect.top - visibleTop) || left.markerRect.top - right.markerRect.top
	    ), fallback = candidates.filter(
	      ({ ownerRect }) => ownerRect.bottom > visibleTop + 1 && ownerRect.top < visibleBottom - 1
	    ).sort(
	      (left, right) => Math.abs(left.ownerRect.top - visibleTop) - Math.abs(right.ownerRect.top - visibleTop) || left.ownerRect.height - right.ownerRect.height
	    ), visibleHeader = visibleHeaders[0], best = visibleHeader ?? fallback[0];
	    if (!best) return null;
	    const marker = visibleHeader ? best.marker : best.owner, markerOffset = visibleHeader ? best.markerRect.top - visibleTop : best.ownerRect.top - visibleTop;
	    return Object.freeze({
	      postNumber: best.postNumber,
	      marker,
	      markerOffset,
	      owner: best.owner,
	      ownerOffset: best.ownerRect.top - visibleTop
	    });
	  }
	  #notifyWindowChange() {
	    for (const listener of [...this.#windowChangeListeners]) listener();
	  }
	  #markUserScrollIntent(direction = 0, direct = !0) {
	    this.#cancelPendingStationaryViewportLock(), this.#releaseStationaryViewport(), direction !== 0 && (this.#lastUserScrollDirection = direction), this.#userScrollSessionActive = !0, this.#lastUserScrollAt = Math.max(
	      Number.EPSILON,
	      finiteNonNegative(this.#now())
	    );
	    for (const listener of [...this.#userScrollIntentListeners]) listener();
	    if (direct)
	      for (const listener of [...this.#directUserScrollIntentListeners])
	        listener();
	  }
	  #markUserScrollProgress() {
	    if (!this.#userScrollSessionActive) return;
	    const now = finiteNonNegative(this.#now());
	    if (this.#lastUserScrollAt <= 0 || now - this.#lastUserScrollAt > USER_SCROLL_SESSION_GAP_MS) {
	      this.#userScrollSessionActive = !1;
	      return;
	    }
	    this.#lastUserScrollAt = Math.max(Number.EPSILON, now);
	  }
	  #finishUserScrollSession() {
	    this.#userScrollSessionActive && (this.#userScrollSessionActive = !1, this.#lastUserScrollAt = Math.max(
	      Number.EPSILON,
	      finiteNonNegative(this.#now())
	    ), this.#requestStationaryViewportLock());
	  }
	  #requestStationaryViewportLock() {
	    this.#stationaryLockPending = !0, this.#stationaryLockSettleFrames = STATIONARY_LOCK_FALLBACK_FRAMES, !this.#viewportMutationAnchor && this.#scheduleStationaryViewportLock();
	  }
	  #scheduleStationaryViewportLock() {
	    if (this.#stationaryLockFrame || this.scope.destroyed || !this.#stationaryLockPending || this.#userScrollSessionActive || this.#viewportMutationAnchor) return;
	    let completed = !1;
	    const handle = this.#requestFrame(() => {
	      if (completed = !0, this.#stationaryLockFrame = 0, !(this.scope.destroyed || !this.#stationaryLockPending || this.#userScrollSessionActive || this.#viewportMutationAnchor)) {
	        if (this.#scrollFrame) {
	          this.#stationaryLockSettleFrames = STATIONARY_LOCK_FALLBACK_FRAMES, this.#scheduleStationaryViewportLock();
	          return;
	        }
	        if (this.#stationaryLockSettleFrames = Math.max(
	          0,
	          this.#stationaryLockSettleFrames - 1
	        ), this.#stationaryLockSettleFrames > 0) {
	          this.#scheduleStationaryViewportLock();
	          return;
	        }
	        this.#stationaryLockPending = !1, this.#lockStationaryViewport();
	      }
	    });
	    completed || (this.#stationaryLockFrame = handle);
	  }
	  #cancelPendingStationaryViewportLock() {
	    this.#stationaryLockPending = !1, this.#stationaryLockSettleFrames = 0, this.#stationaryLockFrame && (this.#cancelFrame(this.#stationaryLockFrame), this.#stationaryLockFrame = 0);
	  }
	  #lockStationaryViewport() {
	    this.#releaseStationaryViewport();
	    const visible = this.#findTopVisiblePost(
	      Array.from(
	        this.#scrollRoot.querySelectorAll(
	          ".ldp-post[data-post-number]"
	        )
	      )
	    );
	    visible && (this.#stationaryAnchor = Object.freeze({
	      postNumber: visible.postNumber,
	      markerRole: visible.marker === visible.owner ? "owner" : "header",
	      markerOffset: visible.markerOffset,
	      ownerOffset: visible.ownerOffset
	    }), this.#syncViewportAnchorClass(), this.#stationaryMutationObserver?.observe(this.#scrollRoot, {
	      attributes: !0,
	      attributeFilter: ["class", "hidden", "style"],
	      characterData: !0,
	      childList: !0,
	      subtree: !0
	    }), this.#observeStationaryContentSize());
	  }
	  #releaseStationaryViewport() {
	    this.#stationaryAnchor = null, this.#stationaryMutationObserver?.disconnect(), this.#stationaryResizeObserver?.disconnect(), this.#syncViewportAnchorClass();
	  }
	  #restoreViewportMutation(token) {
	    const anchor = this.#viewportMutationAnchor;
	    if (!(!anchor || anchor.token !== token))
	      try {
	        const owner = this.#scrollRoot.querySelector(
	          `.ldp-post[data-post-number="${anchor.postNumber}"]`
	        );
	        if (!owner || owner.hidden) return;
	        const header = anchor.markerRole === "header" ? owner.querySelector(":scope > .ldp-post-head") : null, marker = header ?? owner, initialMarkerOffset = header ? anchor.markerOffset : anchor.ownerOffset, currentScrollTop = finiteNonNegative(this.#scrollRoot.scrollTop), expectedMarkerOffset = initialMarkerOffset - (currentScrollTop - anchor.scrollTop), rootRect = this.#scrollRoot.getBoundingClientRect(), visibleTop = rootRect.top + Math.min(
	          rootRect.height,
	          finiteNonNegative(this.#readTopInset())
	        ), correction = marker.getBoundingClientRect().top - visibleTop - expectedMarkerOffset;
	        if (Math.abs(correction) < 0.5) return;
	        const nextOffset = Math.max(0, currentScrollTop + correction), actualOffset = this.#writeScrollRootOffset(nextOffset);
	        this.#scrollOffset = actualOffset, this.#pendingScrollOffset = actualOffset, this.#scrollOffsetDirty = !1;
	      } finally {
	        this.#cancelViewportMutation(token, !0);
	      }
	  }
	  #cancelViewportMutation(token, settleStationary = !1) {
	    if (!(token !== void 0 && this.#viewportMutationAnchor?.token !== token)) {
	      if (this.#viewportMutationAnchor = null, settleStationary && this.#stationaryLockPending && !this.#userScrollSessionActive) {
	        this.#requestStationaryViewportLock(), this.#syncViewportAnchorClass();
	        return;
	      }
	      settleStationary || this.#cancelPendingStationaryViewportLock(), this.#syncViewportAnchorClass();
	    }
	  }
	  #syncViewportAnchorClass() {
	    this.#scrollRoot.classList.toggle(
	      "ldp-stream-viewport-anchor",
	      this.#stationaryAnchor !== null || this.#viewportMutationAnchor !== null
	    );
	  }
	  #observeStationaryContentSize() {
	    const observer = this.#stationaryResizeObserver;
	    if (!observer || !this.#stationaryAnchor) return;
	    observer.disconnect();
	    const stream = this.#scrollRoot.querySelector(
	      ".ldp-virtual-stream"
	    );
	    stream && observer.observe(stream);
	  }
	  #restoreStationaryViewport() {
	    const anchor = this.#stationaryAnchor;
	    if (!anchor || this.#userScrollSessionActive) return;
	    const owner = this.#scrollRoot.querySelector(
	      `.ldp-post[data-post-number="${anchor.postNumber}"]`
	    );
	    if (!owner || owner.hidden) return;
	    const header = anchor.markerRole === "header" ? owner.querySelector(":scope > .ldp-post-head") : null, marker = header ?? owner, expectedOffset = header ? anchor.markerOffset : anchor.ownerOffset, rootRect = this.#scrollRoot.getBoundingClientRect(), visibleTop = rootRect.top + Math.min(
	      rootRect.height,
	      finiteNonNegative(this.#readTopInset())
	    ), correction = marker.getBoundingClientRect().top - visibleTop - expectedOffset;
	    if (Math.abs(correction) < 0.5) return;
	    const nextOffset = Math.max(
	      0,
	      finiteNonNegative(this.#scrollRoot.scrollTop) + correction
	    ), actualOffset = this.#writeScrollRootOffset(nextOffset);
	    this.#scrollOffset = actualOffset, this.#pendingScrollOffset = actualOffset, this.#scrollOffsetDirty = !1;
	  }
	  #writeScrollRootOffset(offset) {
	    this.#internalScrollWriteOffset = offset;
	    let written = !1;
	    if (typeof this.#scrollRoot.scrollTo == "function")
	      try {
	        this.#scrollRoot.scrollTo({ top: offset, behavior: "instant" }), written = !0;
	      } catch {
	      }
	    written || (this.#scrollRoot.scrollTop = offset);
	    const actualOffset = finiteNonNegative(
	      this.#scrollRoot.scrollTop
	    );
	    return this.#internalScrollWriteOffset = actualOffset, actualOffset;
	  }
	  #claimScrollOnlyUserInput() {
	    const actualOffset = finiteNonNegative(this.#scrollRoot.scrollTop), internalOffset = this.#internalScrollWriteOffset;
	    return this.#internalScrollWriteOffset = null, internalOffset !== null && Math.abs(actualOffset - internalOffset) < 0.5 ? !1 : this.#userScrollSessionActive ? !0 : !this.#stationaryAnchor && !this.#stationaryLockPending ? !1 : (this.#markUserScrollIntent(
	      this.#directionOf(actualOffset - this.#scrollOffset),
	      !1
	    ), !0);
	  }
	  #directionOf(delta) {
	    return !Number.isFinite(delta) || Math.abs(delta) < 0.5 ? 0 : delta < 0 ? -1 : 1;
	  }
	  #keyboardDirection(event) {
	    return event.key === "ArrowUp" || event.key === "PageUp" || event.key === "Home" || event.key === " " && event.shiftKey ? -1 : event.key === "ArrowDown" || event.key === "PageDown" || event.key === "End" || event.key === " " ? 1 : 0;
	  }
	  #scheduleScrollCommit() {
	    if (this.#scrollFrame || this.scope.destroyed) return;
	    let completed = !1;
	    const handle = this.#requestFrame(() => {
	      if (completed = !0, this.#scrollFrame = 0, this.scope.destroyed) return;
	      const scrollOffset = this.#refreshPendingScrollOffset();
	      this.#pendingScrollOffset = null, scrollOffset !== this.#scrollOffset && (this.#scrollOffset = scrollOffset, this.#notifyWindowChange());
	    });
	    completed || (this.#scrollFrame = handle);
	  }
	  #refreshPendingScrollOffset() {
	    return this.#scrollOffsetDirty && (this.#pendingScrollOffset = finiteNonNegative(
	      this.#scrollRoot.scrollTop
	    ), this.#scrollOffsetDirty = !1), this.#pendingScrollOffset ?? this.#scrollOffset;
	  }
	}
}, "d7f3e33f99899893dfb0af8fbb9ad65f0c7a63102637490f38df267d5e1ba693");

/* Source: lite/src/topic/reader-topic-scroll-lifecycle.ts */
runtime.register("src/topic/reader-topic-scroll-lifecycle.js", function(module, exports, require) {
	var reader_topic_scroll_lifecycle_exports = {};
	__export(reader_topic_scroll_lifecycle_exports, {
	  ReaderTopicScrollLifecycle: () => ReaderTopicScrollLifecycle
	});
	module.exports = __toCommonJS(reader_topic_scroll_lifecycle_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	class ReaderTopicScrollLifecycle {
	  scope;
	  #readLastUserScrollAt;
	  #readIdleMs;
	  #scheduler;
	  #now;
	  #idleHandle = null;
	  #idlePromise = null;
	  #idleResolve = null;
	  #minimumIdleMs = 0;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#readLastUserScrollAt = options.readLastUserScrollAt, this.#readIdleMs = options.readIdleMs, this.#scheduler = options.scheduler, this.#now = options.now ?? (() => performance.now()), this.scope.add(() => {
	      this.#idleHandle !== null && this.#scheduler.cancel(this.#idleHandle), this.#idleHandle = null;
	      const resolve = this.#idleResolve;
	      this.#idleResolve = null, this.#idlePromise = null, this.#minimumIdleMs = 0, resolve?.();
	    });
	  }
	  lastUserScrollAt() {
	    const value = Number(this.#readLastUserScrollAt());
	    return Number.isFinite(value) && value > 0 ? value : 0;
	  }
	  remainingIdleMs(minimumIdleMs = 120) {
	    const configuredIdleMs = Number(this.#readIdleMs()), idleMs = Math.max(
	      Math.max(0, Number(minimumIdleMs) || 0),
	      Number.isFinite(configuredIdleMs) ? configuredIdleMs : 0
	    ), lastUserScrollAt = this.lastUserScrollAt();
	    return lastUserScrollAt <= 0 ? 0 : Math.max(0, idleMs - (this.#now() - lastUserScrollAt));
	  }
	  isIdle(minimumIdleMs = 120) {
	    return this.remainingIdleMs(minimumIdleMs) <= 0;
	  }
	  waitForIdle(minimumIdleMs = 120) {
	    if (this.scope.destroyed) return Promise.resolve();
	    this.#minimumIdleMs = Math.max(
	      this.#minimumIdleMs,
	      Math.max(0, Number(minimumIdleMs) || 0)
	    );
	    const remainingMs = this.remainingIdleMs(this.#minimumIdleMs);
	    return remainingMs <= 0 ? (this.#minimumIdleMs = 0, Promise.resolve()) : (this.#idlePromise || (this.#idlePromise = new Promise((resolve) => {
	      this.#idleResolve = resolve;
	    })), this.#scheduleIdleCheck(remainingMs), this.#idlePromise);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #scheduleIdleCheck(delayMs) {
	    this.#idleHandle !== null && this.#scheduler.cancel(this.#idleHandle), this.#idleHandle = this.#scheduler.schedule(() => {
	      this.#idleHandle = null;
	      const remainingMs = this.remainingIdleMs(this.#minimumIdleMs);
	      if (!this.scope.destroyed && remainingMs > 0) {
	        this.#scheduleIdleCheck(remainingMs);
	        return;
	      }
	      const resolve = this.#idleResolve;
	      this.#idleResolve = null, this.#idlePromise = null, this.#minimumIdleMs = 0, resolve?.();
	    }, Math.max(0, delayMs));
	  }
	}
}, "78b9e8af68ce9f98c3d2d31a24b01cdee20e2bc67ef8989adee2ad6c4cf6ca2d");

/* Source: lite/src/topic/reader-topic-special-content-feature.ts */
runtime.register("src/topic/reader-topic-special-content-feature.js", function(module, exports, require) {
	var reader_topic_special_content_feature_exports = {};
	__export(reader_topic_special_content_feature_exports, {
	  ReaderTopicSpecialContentFeature: () => ReaderTopicSpecialContentFeature,
	  normalizeReaderSolvedAnswers: () => normalizeReaderSolvedAnswers
	});
	module.exports = __toCommonJS(reader_topic_special_content_feature_exports);
	var import_html_element = require("../dom/html-element.js"), import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_value_record = require("../kernel/value-record.js");
	const EMPTY_RECORD = Object.freeze({});
	function text(value) {
	  return String(value ?? "").trim();
	}
	function postNumber(value) {
	  const numeric = Number((0, import_value_record.valueRecord)(value)?.post_number ?? value);
	  return Number.isSafeInteger(numeric) && numeric > 1 ? numeric : null;
	}
	function acceptedCandidates(topic, posts) {
	  const byPostNumber = /* @__PURE__ */ new Map(), add = (value) => {
	    const number = postNumber(value);
	    number !== null && byPostNumber.set(number, Object.freeze({
	      ...byPostNumber.get(number) ?? {},
	      ...(0, import_value_record.valueRecord)(value) ?? {},
	      post_number: number
	    }));
	  };
	  if (Array.isArray(topic.accepted_answers))
	    for (const answer of topic.accepted_answers) add(answer);
	  add(topic.accepted_answer);
	  for (const postValue of posts) {
	    const post = (0, import_value_record.valueRecord)(postValue);
	    post?.accepted_answer === !0 && add(post);
	  }
	  return Object.freeze([...byPostNumber.values()].sort((left, right) => Number(left.post_number) - Number(right.post_number)));
	}
	function normalizeReaderSolvedAnswers(topicValue, posts, presentation) {
	  const topic = (0, import_value_record.valueRecord)(topicValue) ?? EMPTY_RECORD, postsByNumber = /* @__PURE__ */ new Map();
	  for (const postValue of posts) {
	    const post = (0, import_value_record.valueRecord)(postValue), number = Number(post?.post_number);
	    post && Number.isSafeInteger(number) && number > 0 && postsByNumber.set(number, post);
	  }
	  return Object.freeze(
	    acceptedCandidates(topic, posts).map((candidate) => {
	      const number = Number(candidate.post_number), canonical = postsByNumber.get(number) ?? EMPTY_RECORD, username = text(candidate.username ?? canonical.username), name = text(
	        candidate.name ?? canonical.name ?? username
	      ) || "已解决回复", avatarTemplate = text(
	        candidate.avatar_template ?? canonical.avatar_template
	      );
	      return Object.freeze({
	        postNumber: number,
	        username,
	        name,
	        avatarSource: presentation.avatarSource(avatarTemplate, 32),
	        createdAt: text(
	          candidate.created_at ?? canonical.created_at
	        ),
	        cooked: text(candidate.cooked ?? canonical.cooked),
	        excerpt: text(candidate.excerpt ?? canonical.excerpt)
	      });
	    })
	  );
	}
	function specialBadges(post) {
	  const badges = [], add = (label, tone = "", title = "", icon = "") => {
	    badges.push(Object.freeze({ label, tone, title, icon }));
	  }, postType = Number(post.post_type);
	  return postType === 4 && add("私信回复"), postType === 2 && add("管理操作", "warn"), post.wiki === !0 && add("Wiki"), post.deleted_at && add("已删除", "danger"), post.locked === !0 && add("已锁定", "warn"), Object.freeze(badges);
	}
	function postIdentityBadge(post) {
	  const username = text(post.username).toLocaleLowerCase();
	  if (Number(post.post_type) === 3 || username === "system")
	    return Object.freeze({
	      label: "系统",
	      title: "系统账户",
	      icon: "settings"
	    });
	  if (post.moderator === !0 || post.group_moderator === !0)
	    return Object.freeze({
	      label: "版主",
	      title: "版主",
	      icon: "shield-halved"
	    });
	  const notice = (0, import_value_record.valueRecord)(post.notice), noticeType = text(notice?.type);
	  if (noticeType === "new_user")
	    return Object.freeze({
	      label: "新用户",
	      title: "新用户,首次发帖",
	      icon: "user-plus"
	    });
	  if (noticeType === "returning_user")
	    return Object.freeze({
	      label: "回归用户",
	      title: "回归用户,久未发帖",
	      icon: "rotate-ccw"
	    });
	  if (noticeType !== "custom") return null;
	  const source = `${text(notice?.raw)} ${text(notice?.cooked)}`;
	  return /\bpremium\b/i.test(source) ? Object.freeze({ label: "Premium", title: "Premium", icon: "check" }) : Object.freeze({ label: "富可敌国", title: "富可敌国", icon: "tag" });
	}
	const SYSTEM_ACTION_LABELS = Object.freeze({
	  closed: "主题已关闭",
	  opened: "主题已重新开放",
	  archived: "主题已归档",
	  unarchived: "主题已取消归档",
	  pinned: "主题已置顶",
	  unpinned: "主题已取消置顶",
	  autoclosed: "主题已自动关闭",
	  "autoclosed.enabled": "主题已自动关闭",
	  "autoclosed.disabled": "主题已自动重新开放",
	  split_topic: "帖子已拆分到新主题",
	  merged: "主题已合并",
	  moved: "帖子已移动",
	  visible: "主题已公开",
	  invisible: "主题已隐藏",
	  "visible.enabled": "主题已公开",
	  "visible.disabled": "主题已取消公开",
	  renamed: "主题标题已修改",
	  assigned: "已指定"
	}), SYSTEM_ACTION_ICONS = Object.freeze({
	  visible: "check",
	  invisible: "eye-off",
	  "visible.enabled": "check",
	  "visible.disabled": "eye-off",
	  assigned: "user-plus"
	});
	class ReaderTopicSpecialContentFeature {
	  scope;
	  #document;
	  #session;
	  #presentation;
	  #relativeTime;
	  #navigate;
	  #renderIcon;
	  #actions;
	  #commands;
	  #descriptors;
	  #models;
	  #loadPostVotingComments;
	  #onBodyLayerChanged;
	  #onError;
	  #boundViews = /* @__PURE__ */ new WeakSet();
	  #starterViews = /* @__PURE__ */ new Set();
	  #views = /* @__PURE__ */ new Map();
	  #expandedComments = /* @__PURE__ */ new WeakSet();
	  #pendingPostIds = /* @__PURE__ */ new Map();
	  #loadingCommentPostIds = /* @__PURE__ */ new Set();
	  constructor(options) {
	    this.#document = options.document, this.#session = options.session, this.#presentation = options.presentation, this.#relativeTime = options.relativeTime, this.#navigate = options.navigate, this.#renderIcon = options.renderIcon ?? null, this.#actions = options.actions ?? null, this.#commands = options.commands ?? null, this.#descriptors = options.descriptors ?? null, this.#models = options.models ?? null, this.#loadPostVotingComments = options.loadPostVotingComments ?? null, this.#onBodyLayerChanged = options.onBodyLayerChanged ?? null, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), (options.presentationChanges ?? this.#session.changes).subscribe(() => {
	      for (const view of [...this.#starterViews]) {
	        if (view.scope.destroyed) {
	          this.#starterViews.delete(view);
	          continue;
	        }
	        this.#renderSolved(view) && this.#notifyBodyLayerChanged(view);
	      }
	    }, this.scope), this.#actions?.events.subscribe((event) => {
	      const postIds = event.presentation?.postIds ?? [];
	      if (event.phase === "pending")
	        for (const postId of postIds)
	          this.#pendingPostIds.set(
	            postId,
	            (this.#pendingPostIds.get(postId) ?? 0) + 1
	          );
	      else if (event.phase === "settled")
	        for (const postId of postIds) {
	          const next = (this.#pendingPostIds.get(postId) ?? 1) - 1;
	          next > 0 ? this.#pendingPostIds.set(postId, next) : this.#pendingPostIds.delete(postId);
	        }
	      else
	        return;
	      this.#refreshInteractiveViews(new Set(postIds));
	    }, this.scope), this.scope.add(() => {
	      this.#starterViews.clear(), this.#views.clear(), this.#pendingPostIds.clear(), this.#loadingCommentPostIds.clear();
	    });
	  }
	  afterRender(postValue, view) {
	    const post = (0, import_value_record.valueRecord)(postValue) ?? EMPTY_RECORD;
	    this.#views.set(view, Number(post.post_number)), this.#bindView(view), this.#renderRootState(post, view), this.#renderSpecialBadges(post, view), this.#renderNotice(post, view), this.#renderSystemAction(post, view), this.#renderPostEvent(post, view), Number(post.post_number) === 1 && (this.#starterViews.add(view), this.#renderSolved(view)), this.#renderPostVoting(post, view);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #bindView(view) {
	    this.#boundViews.has(view) || (this.#boundViews.add(view), view.scope.listen(view.slots.root, "click", (event) => {
	      this.#onClick(view, event);
	    }), view.scope.listen(view.slots.root, "submit", (event) => {
	      this.#onSubmit(view, event);
	    }), view.scope.add(() => {
	      this.#starterViews.delete(view), this.#views.delete(view);
	    }));
	  }
	  #refreshInteractiveViews(postIds) {
	    for (const [view, number] of [...this.#views]) {
	      if (view.scope.destroyed) {
	        this.#views.delete(view);
	        continue;
	      }
	      const post = this.#session.postByNumber(number);
	      if (!post) continue;
	      const postId = Number(post.id);
	      if (postIds && !postIds.has(postId)) continue;
	      const source = (0, import_value_record.valueRecord)(post) ?? EMPTY_RECORD;
	      this.#renderInteractiveBody(source, view) && this.#notifyBodyLayerChanged(view);
	    }
	  }
	  #currentPost(view) {
	    const number = this.#views.get(view) ?? view.postNumber;
	    return this.#session.postByNumber(number) ?? null;
	  }
	  #onClick(view, event) {
	    const target = event.target, solved = target?.closest(
	      "[data-reader-solved-post-number]"
	    );
	    if (solved && view.slots.bodyLayer.contains(solved)) {
	      const number = Number(solved.dataset.readerSolvedPostNumber);
	      if (!Number.isSafeInteger(number) || number < 2) return;
	      event.preventDefault(), new Promise((resolve) => {
	        resolve(this.#navigate(number));
	      }).catch((error) => {
	        !this.scope.destroyed && !view.scope.destroyed && this.#onError(error);
	      });
	      return;
	    }
	    const post = this.#currentPost(view);
	    if (!post) return;
	    const source = (0, import_value_record.valueRecord)(post) ?? EMPTY_RECORD, postId = Number(source.id);
	    if (!Number.isSafeInteger(postId) || postId < 1) return;
	    if (target?.closest(
	      "[data-pv-comments-toggle]"
	    )) {
	      event.preventDefault(), this.#expandedComments.has(view) ? this.#expandedComments.delete(view) : this.#expandedComments.add(view), this.#renderPostVoting(source, view), this.#notifyBodyLayerChanged(view);
	      return;
	    }
	    if (target?.closest(
	      "[data-pv-comments-more]"
	    )) {
	      event.preventDefault(), this.#loadMorePostVotingComments(post, view);
	      return;
	    }
	    const vote = target?.closest("[data-pv-vote]");
	    if (vote) {
	      event.preventDefault();
	      const direction = text(vote.dataset.pvVote);
	      if (direction !== "up" && direction !== "down") return;
	      const current = text(source.post_voting_user_voted_direction);
	      this.#runAction(() => {
	        if (!this.#descriptors || !this.#commands)
	          throw new Error("Post Voting 动作端口尚未就绪");
	        const mutation = this.#descriptors.postVotingVote({
	          postId,
	          direction,
	          remove: current === direction
	        });
	        return this.#commands.postVotingVote(postId, mutation);
	      });
	      return;
	    }
	    const commentVote = target?.closest(
	      "[data-pv-comment-vote]"
	    );
	    if (commentVote) {
	      event.preventDefault();
	      const commentNode = commentVote.closest(
	        "[data-pv-comment-id]"
	      ), commentId = Number(commentNode?.dataset.pvCommentId);
	      if (!Number.isSafeInteger(commentId) || commentId < 1) return;
	      const comment = this.#postVotingComments(source).find((entry) => Number(entry.id) === commentId);
	      this.#runAction(() => {
	        if (!this.#descriptors || !this.#commands)
	          throw new Error("Post Voting 评论动作端口尚未就绪");
	        const remove = comment?.user_voted === !0, mutation = this.#descriptors.postVotingCommentVote({
	          commentId,
	          remove
	        });
	        return this.#commands.postVotingCommentVote(
	          postId,
	          commentId,
	          remove,
	          mutation
	        );
	      });
	      return;
	    }
	    const attendance = target?.closest(
	      "[data-event-status]"
	    );
	    if (!attendance) return;
	    event.preventDefault();
	    const status = text(attendance.dataset.eventStatus), eventData = (0, import_value_record.valueRecord)(source.event), eventId = Number(eventData?.id) || postId;
	    !eventData || !Number.isSafeInteger(eventId) || eventId < 1 || this.#runAction(() => {
	      if (!this.#descriptors || !this.#commands || !this.#models)
	        throw new Error("Discourse 活动动作端口尚未就绪");
	      const invitee = (0, import_value_record.valueRecord)(eventData.watching_invitee), eventModel = this.#models.createPostEvent(eventData, postId), mutation = this.#descriptors.eventAttendance({
	        eventId,
	        event: eventModel,
	        status,
	        alreadyInvited: Number(invitee?.id) > 0
	      });
	      return this.#commands.eventAttendance(postId, mutation);
	    });
	  }
	  #onSubmit(view, event) {
	    const form = event.target?.closest(
	      ".ldp-pv-comment-form"
	    );
	    if (!form || !view.slots.root.contains(form)) return;
	    event.preventDefault();
	    const post = this.#currentPost(view), source = (0, import_value_record.valueRecord)(post), postId = Number(source?.id);
	    if (!post || !Number.isSafeInteger(postId) || postId < 1) return;
	    const input = form.querySelector(
	      ".ldp-pv-comment-input"
	    ), raw = text(input?.value);
	    raw && this.#runAction(() => {
	      if (!this.#descriptors || !this.#commands)
	        throw new Error("Post Voting 评论动作端口尚未就绪");
	      const mutation = this.#descriptors.postVotingCommentCreate({
	        postId,
	        raw
	      });
	      return this.#commands.postVotingCommentCreate(postId, mutation);
	    }).then(() => {
	      !this.scope.destroyed && !view.scope.destroyed && input && (input.value = "");
	    });
	  }
	  async #runAction(create) {
	    try {
	      if (!this.#actions) throw new Error("楼层动作控制器尚未就绪");
	      await this.#actions.dispatch(create());
	    } catch (error) {
	      this.scope.destroyed || this.#onError(error);
	    }
	  }
	  async #loadMorePostVotingComments(post, view) {
	    const source = (0, import_value_record.valueRecord)(post) ?? EMPTY_RECORD, postId = Number(source.id);
	    if (!this.#loadPostVotingComments || !Number.isSafeInteger(postId) || postId < 1 || this.#loadingCommentPostIds.has(postId)) return;
	    const loaded = this.#postVotingComments(source), afterCommentId = Number(loaded.at(-1)?.id) || 0;
	    this.#loadingCommentPostIds.add(postId), this.#renderPostVoting(source, view), this.#notifyBodyLayerChanged(view);
	    try {
	      const payload = await this.#loadPostVotingComments(
	        postId,
	        afterCommentId
	      ), payloadRecord = (0, import_value_record.valueRecord)(payload), incomingSource = Array.isArray(payload) ? payload : Array.isArray(payloadRecord?.comments) ? payloadRecord.comments : [], merged = /* @__PURE__ */ new Map();
	      for (const comment of [...loaded, ...incomingSource]) {
	        const candidate = (0, import_value_record.valueRecord)(comment), id = Number(candidate?.id);
	        candidate && Number.isSafeInteger(id) && id > 0 && merged.set(id, Object.freeze({ ...candidate }));
	      }
	      this.#session.ingestPosts([
	        {
	          ...source,
	          post_voting_comments: Object.freeze([...merged.values()])
	        }
	      ], "action-response");
	    } catch (error) {
	      !this.scope.destroyed && !view.scope.destroyed && this.#onError(error);
	    } finally {
	      this.#loadingCommentPostIds.delete(postId);
	      const current = this.#session.postByNumber(Number(source.post_number));
	      current && !view.scope.destroyed && (this.#renderPostVoting((0, import_value_record.valueRecord)(current) ?? EMPTY_RECORD, view), this.#notifyBodyLayerChanged(view));
	    }
	  }
	  #renderInteractiveBody(post, view) {
	    const selector = ":scope > :is(.ldp-pv-votes,.ldp-pv-comments,.ldp-event-card)", hadInteractiveBody = !!view.slots.bodyLayer.querySelector(selector);
	    return this.#renderPostEvent(post, view), this.#renderPostVoting(post, view), hadInteractiveBody || !!view.slots.bodyLayer.querySelector(selector);
	  }
	  #notifyBodyLayerChanged(view) {
	    if (!(this.scope.destroyed || view.scope.destroyed))
	      try {
	        this.#onBodyLayerChanged?.(view);
	      } catch (error) {
	        this.#onError(error);
	      }
	  }
	  #postVotingComments(post) {
	    const source = Array.isArray(post.post_voting_comments) ? post.post_voting_comments : Array.isArray(post.comments) ? post.comments : [];
	    return Object.freeze(source.map((value) => (0, import_value_record.valueRecord)(value)).filter((value) => value !== null));
	  }
	  #renderSolved(view) {
	    if (view.scope.destroyed) return !1;
	    const answers = normalizeReaderSolvedAnswers(
	      this.#session.topic,
	      this.#session.cachedPosts(),
	      this.#presentation
	    ), previous = view.slots.bodyLayer.querySelector(
	      ":scope > .ldp-solved-card"
	    );
	    if (!answers.length)
	      return previous?.remove(), previous !== null;
	    const card = (0, import_html_element.htmlElement)(this.#document, "section", "ldp-solved-card"), heading = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-solved-head"), icon = this.#icon("check");
	    icon && heading.append(icon);
	    const headingLabel = answers.length > 1 ? `✓ 已解决 · ${answers.length} 个答案` : "✓ 已解决", headingText = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-solved-label",
	      icon ? headingLabel.replace(/^✓\s*/, "") : headingLabel
	    );
	    heading.append(headingText), card.append(heading);
	    for (const answer of answers)
	      card.append(this.#answerNode(answer));
	    return previous?.isEqualNode(card) ? !1 : (previous ? previous.replaceWith(card) : view.slots.bodyLayer.append(card), !0);
	  }
	  #answerNode(answer) {
	    const body = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-solved-body");
	    body.dataset.solvedPostNumber = String(answer.postNumber);
	    const authorRow = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-solved-author-row"
	    ), profileHref = this.#presentation.userHref(answer.username);
	    if (answer.avatarSource) {
	      const avatarLink = this.#document.createElement(
	        profileHref ? "a" : "span"
	      );
	      avatarLink.className = "ldp-user-link", profileHref && avatarLink.setAttribute("href", profileHref), answer.username && (avatarLink.dataset.userCard = answer.username);
	      const avatar = (0, import_html_element.htmlElement)(this.#document, "img", "ldp-solved-avatar");
	      avatar.src = answer.avatarSource, avatar.alt = "", avatar.loading = "lazy", avatar.decoding = "async", avatarLink.append(avatar), authorRow.append(avatarLink);
	    }
	    const author = this.#document.createElement(
	      profileHref ? "a" : "span"
	    );
	    if (author.className = "ldp-user-link ldp-solved-author", author.textContent = answer.name, profileHref && author.setAttribute("href", profileHref), answer.username && (author.dataset.userCard = answer.username), authorRow.append(author), answer.username) {
	      const username = (0, import_html_element.htmlElement)(
	        this.#document,
	        "span",
	        "ldp-solved-username",
	        `@${answer.username}`
	      );
	      authorRow.append(username);
	    }
	    const relative = answer.createdAt ? this.#relativeTime(answer.createdAt) : "";
	    if (relative) {
	      const time = (0, import_html_element.htmlElement)(this.#document, "span", "", `· ${relative}`);
	      authorRow.append(time);
	    }
	    const floor = (0, import_html_element.htmlElement)(
	      this.#document,
	      "button",
	      "ldp-solved-floor ldp-jump-self",
	      `#${answer.postNumber}`
	    );
	    floor.type = "button", floor.dataset.readerSolvedPostNumber = String(answer.postNumber), floor.setAttribute("aria-label", `跳到楼层 #${answer.postNumber}`), authorRow.append(floor), body.append(authorRow);
	    const excerpt = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-solved-excerpt ldp-content cooked"
	    );
	    answer.cooked ? excerpt.innerHTML = answer.cooked : excerpt.textContent = answer.excerpt || "查看被采纳的完整回复。", body.append(excerpt);
	    const jump = (0, import_html_element.htmlElement)(
	      this.#document,
	      "button",
	      "ldp-solved-jump ldp-jump-self",
	      "阅读更多"
	    );
	    return jump.type = "button", jump.dataset.readerSolvedPostNumber = String(answer.postNumber), body.append(jump), body;
	  }
	  #renderPostVoting(post, view) {
	    view.slots.bodyLayer.querySelector(":scope > .ldp-pv-votes")?.remove(), view.slots.bodyLayer.querySelector(":scope > .ldp-pv-comments")?.remove();
	    const isAnswer = ((0, import_value_record.valueRecord)(this.#session.topic) ?? EMPTY_RECORD).is_post_voting === !0 && Number(post.post_number) !== 1;
	    if (view.slots.root.classList.toggle("ldp-post-voting-answer", isAnswer), !isAnswer) return;
	    const postId = Number(post.id), pending = this.#pendingPostIds.has(postId), votes = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-pv-votes"), direction = text(post.post_voting_user_voted_direction);
	    for (const [value, label, iconName] of [
	      ["up", "赞同", "chevron-up"],
	      ["down", "反对", "chevron-down"]
	    ]) {
	      const button = this.#document.createElement("button");
	      button.type = "button", button.className = `ldp-pv-vote${direction === value ? " on" : ""}`, button.dataset.pvVote = value, button.setAttribute("aria-label", label), button.disabled = pending || !this.#actions;
	      const icon = this.#icon(iconName);
	      if (icon ? button.append(icon) : button.textContent = value === "up" ? "↑" : "↓", votes.append(button), value === "up") {
	        const score = (0, import_html_element.htmlElement)(
	          this.#document,
	          "span",
	          "ldp-pv-score",
	          String(Math.max(0, Number(post.post_voting_vote_count) || 0))
	        );
	        votes.append(score);
	      }
	    }
	    view.slots.bodyLayer.prepend(votes);
	    const comments = this.#postVotingComments(post), count = Math.max(
	      0,
	      Number(post.comments_count) || comments.length
	    ), host = (0, import_html_element.htmlElement)(this.#document, "section", "ldp-pv-comments"), toggle = (0, import_html_element.htmlElement)(
	      this.#document,
	      "button",
	      "ldp-pv-comments-toggle"
	    );
	    toggle.type = "button", toggle.dataset.pvCommentsToggle = "";
	    const expanded = this.#expandedComments.has(view);
	    toggle.setAttribute("aria-expanded", String(expanded)), toggle.setAttribute(
	      "aria-label",
	      expanded ? "收起评论" : "展开评论"
	    );
	    const toggleIcon = this.#icon("message-square");
	    toggleIcon && toggle.append(toggleIcon);
	    const toggleCount = (0, import_html_element.htmlElement)(this.#document, "span", "", String(count));
	    toggle.append(toggleCount), host.append(toggle);
	    const body = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-pv-comments-body");
	    body.hidden = !expanded;
	    for (const comment of comments)
	      body.append(this.#postVotingCommentNode(comment, pending));
	    if (comments.length < count && this.#loadPostVotingComments) {
	      const more = (0, import_html_element.htmlElement)(
	        this.#document,
	        "button",
	        "ldp-pv-comments-more"
	      );
	      more.type = "button", more.dataset.pvCommentsMore = "", more.disabled = this.#loadingCommentPostIds.has(postId), more.textContent = more.disabled ? "正在加载…" : "加载更多评论", body.append(more);
	    }
	    if (this.#models?.currentUser()) {
	      const form = (0, import_html_element.htmlElement)(this.#document, "form", "ldp-pv-comment-form"), input = (0, import_html_element.htmlElement)(this.#document, "input", "ldp-pv-comment-input");
	      input.name = "raw", input.autocomplete = "off", input.placeholder = "写评论…", input.required = !0, input.disabled = pending;
	      const submit = (0, import_html_element.htmlElement)(
	        this.#document,
	        "button",
	        "ldp-pv-comment-submit",
	        "发送"
	      );
	      submit.type = "submit", submit.disabled = pending, form.append(input, submit), body.append(form);
	    }
	    host.append(body), view.slots.bodyLayer.append(host);
	  }
	  #postVotingCommentNode(comment, pending) {
	    const user = (0, import_value_record.valueRecord)(comment.user) ?? EMPTY_RECORD, username = text(comment.username ?? user.username), name = text(comment.name ?? user.name ?? username) || "用户", node = (0, import_html_element.htmlElement)(this.#document, "article", "ldp-pv-comment"), commentId = Number(comment.id);
	    Number.isSafeInteger(commentId) && commentId > 0 && (node.dataset.pvCommentId = String(commentId));
	    const avatarTemplate = text(
	      user.avatar_template ?? comment.avatar_template
	    ), avatarSource = this.#presentation.avatarSource(avatarTemplate, 28), avatarHost = this.#document.createElement(
	      username ? "a" : "span"
	    );
	    if (avatarHost.className = "ldp-user-link", username) {
	      avatarHost.dataset.userCard = username;
	      const href = this.#presentation.userHref(username);
	      href && avatarHost.setAttribute("href", href);
	    }
	    if (avatarSource) {
	      const avatar = (0, import_html_element.htmlElement)(
	        this.#document,
	        "img",
	        "ldp-pv-comment-avatar"
	      );
	      avatar.src = avatarSource, avatar.alt = "", avatar.loading = "lazy", avatar.decoding = "async", avatarHost.append(avatar);
	    }
	    node.append(avatarHost);
	    const content = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-pv-comment-body"), meta = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-pv-comment-meta",
	      [
	        name,
	        username ? `@${username}` : "",
	        text(comment.created_at) ? this.#relativeTime(text(comment.created_at)) : ""
	      ].filter(Boolean).join(" · ")
	    ), cooked = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-content cooked"), cookedValue = text(comment.cooked);
	    cookedValue ? cooked.innerHTML = cookedValue : cooked.textContent = text(comment.raw), content.append(meta, cooked), node.append(content);
	    const vote = this.#document.createElement("button");
	    vote.type = "button", vote.className = `ldp-pv-comment-vote${comment.user_voted === !0 ? " on" : ""}`, vote.dataset.pvCommentVote = "", vote.setAttribute("aria-label", "赞同评论"), vote.disabled = pending || !this.#actions || !Number.isSafeInteger(commentId) || commentId < 1;
	    const icon = this.#icon("chevron-up");
	    icon && vote.append(icon);
	    const count = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "",
	      String(Math.max(0, Number(comment.post_voting_vote_count) || 0))
	    );
	    return vote.append(count), node.append(vote), node;
	  }
	  #renderPostEvent(post, view) {
	    view.slots.bodyLayer.querySelector(":scope > .ldp-event-card")?.remove();
	    const event = (0, import_value_record.valueRecord)(post.event);
	    if (!event) return;
	    const postId = Number(post.id), pending = this.#pendingPostIds.has(postId), card = (0, import_html_element.htmlElement)(this.#document, "section", "ldp-event-card"), title = (0, import_html_element.htmlElement)(
	      this.#document,
	      "h3",
	      "ldp-event-title",
	      text(event.name) || "活动"
	    );
	    card.append(title);
	    const grid = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-event-grid"), dateLabel = this.#eventDateLabel(event);
	    if (dateLabel) {
	      const date = this.#document.createElement("div"), strong = (0, import_html_element.htmlElement)(this.#document, "b", "", dateLabel);
	      date.append(strong);
	      const timezone = text(event.timezone);
	      if (timezone) {
	        const zone = (0, import_html_element.htmlElement)(
	          this.#document,
	          "span",
	          "ldp-event-meta",
	          ` ${timezone}`
	        );
	        date.append(zone);
	      }
	      grid.append(date);
	    }
	    const locationRecord = (0, import_value_record.valueRecord)(event.location), location = typeof event.location == "string" ? text(event.location) : text(
	      locationRecord?.name ?? locationRecord?.address ?? locationRecord?.display
	    );
	    if (location) {
	      const place = (0, import_html_element.htmlElement)(
	        this.#document,
	        "div",
	        "",
	        `地点:${location}`
	      );
	      grid.append(place);
	    }
	    const description = text(event.description_html);
	    if (description) {
	      const detail = (0, import_html_element.htmlElement)(this.#document, "div", "cooked");
	      detail.innerHTML = description, grid.append(detail);
	    }
	    const stats = (0, import_value_record.valueRecord)(event.stats) ?? EMPTY_RECORD, statsNode = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-event-meta"), going = Math.max(0, Number(stats.going) || 0), interested = Math.max(0, Number(stats.interested) || 0), maxAttendees = Math.max(0, Number(event.max_attendees) || 0);
	    statsNode.textContent = [
	      `参加 ${going}`,
	      `感兴趣 ${interested}`,
	      maxAttendees ? `名额 ${going}/${maxAttendees}` : "",
	      event.is_ongoing === !0 ? "进行中" : "",
	      event.is_expired === !0 ? "已结束" : ""
	    ].filter(Boolean).join(" · "), grid.append(statsNode), card.append(grid);
	    const actions = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-event-actions"), watching = (0, import_value_record.valueRecord)(event.watching_invitee) ?? EMPTY_RECORD, currentStatus = text(watching.status), closed = event.is_closed === !0 || event.is_expired === !0 || event.can_update_attendance === !1;
	    for (const [status, label] of [
	      ["going", "参加"],
	      ["interested", "感兴趣"],
	      ["not_going", "不参加"]
	    ]) {
	      const button = (0, import_html_element.htmlElement)(
	        this.#document,
	        "button",
	        `ldp-event-action${currentStatus === status ? " on" : ""}`,
	        label
	      );
	      button.type = "button", button.dataset.eventStatus = status, button.disabled = closed || pending || !this.#actions || !this.#models, actions.append(button);
	    }
	    const calendarUrl = text(event.ics_url ?? event.calendar_url);
	    if (/^(?:https?:\/\/|\/)/i.test(calendarUrl)) {
	      const calendar = (0, import_html_element.htmlElement)(
	        this.#document,
	        "a",
	        "ldp-event-ics",
	        "下载日历"
	      );
	      calendar.href = calendarUrl, calendar.download = "", actions.append(calendar);
	    }
	    card.append(actions), view.slots.bodyLayer.append(card);
	  }
	  #eventDateLabel(event) {
	    const options = {
	      year: "numeric",
	      month: "short",
	      day: "numeric",
	      ...event.all_day === !0 ? {} : { hour: "2-digit", minute: "2-digit" }
	    }, format = (value) => {
	      const date = new Date(text(value));
	      return Number.isFinite(date.getTime()) ? date.toLocaleString("zh-CN", options) : "";
	    };
	    return [format(event.starts_at), format(event.ends_at)].filter(Boolean).join(" — ");
	  }
	  #icon(name) {
	    return (0, import_reader_icon.renderReaderIcon)(this.#document, name, this.#renderIcon);
	  }
	  #renderRootState(post, view) {
	    const postType = Number(post.post_type), actionCode = text(post.action_code);
	    view.slots.root.classList.toggle("ldp-whisper", postType === 4), view.slots.root.classList.toggle(
	      "ldp-system-post",
	      postType === 2 || postType === 3
	    ), view.slots.root.classList.toggle(
	      "ldp-system-action-compact",
	      (postType === 2 || postType === 3) && actionCode === "assigned"
	    ), this.#renderIdentityBadge(post, view);
	  }
	  #renderIdentityBadge(post, view) {
	    view.slots.header.querySelector(":scope > .ldp-new-user-badge")?.remove();
	    const noticeType = text((0, import_value_record.valueRecord)(post.notice)?.type);
	    view.slots.root.classList.toggle(
	      "ldp-new-user",
	      noticeType === "new_user"
	    );
	    const identity = postIdentityBadge(post);
	    if (!identity) return;
	    const badge = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-new-user-badge"
	    );
	    badge.dataset.ldpTooltipLabel = identity.title, badge.setAttribute("role", "img"), badge.setAttribute("aria-label", identity.title), badge.append(
	      this.#icon(identity.icon),
	      (0, import_html_element.htmlElement)(this.#document, "span", "", identity.label)
	    );
	    const username = view.slots.header.querySelector(":scope > .ldp-user");
	    username ? username.after(badge) : view.slots.header.append(badge);
	  }
	  #renderSpecialBadges(post, view) {
	    view.slots.bodyLayer.querySelector(":scope > .ldp-special-badges")?.remove();
	    const badges = specialBadges(post);
	    if (!badges.length) return;
	    const host = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-special-badges");
	    for (const badge of badges) {
	      const node = this.#document.createElement("span");
	      node.className = `ldp-special-badge ${badge.tone}`.trim(), badge.title && (node.dataset.ldpTooltipLabel = badge.title), badge.icon && node.append(this.#icon(badge.icon));
	      const label = (0, import_html_element.htmlElement)(this.#document, "span", "", badge.label);
	      node.append(label), host.append(node);
	    }
	    view.slots.bodyLayer.prepend(host);
	  }
	  #renderNotice(post, view) {
	    view.slots.bodyLayer.querySelector(":scope > .ldp-special-notice")?.remove();
	    const notice = (0, import_value_record.valueRecord)(post.notice), noticeType = text(notice?.type);
	    if (["new_user", "returning_user", "custom"].includes(noticeType))
	      return;
	    const label = typeof post.notice == "string" ? text(post.notice) : text(notice?.text ?? noticeType);
	    if (!label) return;
	    const node = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-special-notice",
	      label
	    );
	    view.slots.bodyLayer.prepend(node);
	  }
	  #renderSystemAction(post, view) {
	    view.slots.bodyLayer.querySelector(":scope > .ldp-system-action")?.remove();
	    const postType = Number(post.post_type), actionCode = text(post.action_code);
	    if (postType !== 2 && postType !== 3 || !actionCode) return;
	    const node = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-system-action");
	    node.append(this.#icon(
	      SYSTEM_ACTION_ICONS[actionCode] ?? "settings"
	    ));
	    const label = SYSTEM_ACTION_LABELS[actionCode] ?? `系统操作:${actionCode.replace(/[._]/g, " ")}`, content = this.#document.createElement("span");
	    if (content.append(this.#document.createTextNode(label)), actionCode === "assigned") {
	      const who = (0, import_value_record.valueRecord)(post.action_code_who), username = text(who?.username ?? post.action_code_who).replace(/^@/, "");
	      if (username) {
	        content.append(this.#document.createTextNode("给 "));
	        const user = (0, import_html_element.htmlElement)(
	          this.#document,
	          "a",
	          "ldp-user-link ldp-system-action-user",
	          `@${username}`
	        );
	        user.dataset.userCard = username;
	        const href = this.#presentation.userHref(username);
	        href && (user.href = href), content.append(user);
	      }
	    }
	    node.append(content), view.slots.bodyLayer.prepend(node);
	  }
	}
}, "b149c5c98ca216145de9530178a7afdece9da9bd44cdfb82c196ca70d50a678d");

/* Source: lite/src/topic/reader-topic-timeline-controller.ts */
runtime.register("src/topic/reader-topic-timeline-controller.js", function(module, exports, require) {
	var reader_topic_timeline_controller_exports = {};
	__export(reader_topic_timeline_controller_exports, {
	  ReaderTopicTimelineController: () => ReaderTopicTimelineController
	});
	module.exports = __toCommonJS(reader_topic_timeline_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function normalizedTotal(value) {
	  const numeric = Math.floor(Number(value));
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : 1;
	}
	function normalizedNavigablePosts(values, totalPostCount) {
	  if (!values?.length) return null;
	  const postNumbers = /* @__PURE__ */ new Set();
	  for (const value of values) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(value);
	    postNumber !== null && postNumber <= totalPostCount && postNumbers.add(postNumber);
	  }
	  return postNumbers.size ? Object.freeze([...postNumbers].sort((left, right) => left - right)) : null;
	}
	function clampedPostNumber(value, totalPostCount) {
	  return (0, import_identifiers.discoursePostReference)({
	    post_number: Math.max(1, Math.min(totalPostCount, Math.floor(value) || 1))
	  }).postNumber;
	}
	function snapshotsEqual(left, right) {
	  if (left.currentPostNumber !== right.currentPostNumber || left.totalPostCount !== right.totalPostCount || left.progress !== right.progress || left.pendingPostNumber !== right.pendingPostNumber)
	    return !1;
	  const leftPosts = left.navigablePostNumbers, rightPosts = right.navigablePostNumbers;
	  return leftPosts === rightPosts || leftPosts !== null && rightPosts !== null && leftPosts.length === rightPosts.length && leftPosts.every((postNumber, index) => postNumber === rightPosts[index]);
	}
	class ReaderTopicTimelineController {
	  scope;
	  changes = new import_signal.Signal();
	  #navigation;
	  #readTotalPostCount;
	  #readNavigablePostNumbers;
	  #readNavigablePostNumbersComplete;
	  #onError;
	  #snapshot;
	  #jumpEpoch = 0;
	  #heldVisiblePostNumber = null;
	  #visiblePostHoldGeneration = 0;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#navigation = options.navigation, this.#readTotalPostCount = options.readTotalPostCount, this.#readNavigablePostNumbers = options.readNavigablePostNumbers ?? (() => null), this.#readNavigablePostNumbersComplete = options.readNavigablePostNumbersComplete ?? (() => !0), this.#onError = options.onError ?? (() => {
	    }), this.#snapshot = this.#derive(
	      options.initialPostNumber ?? 1,
	      null
	    ), this.#navigation.changes.subscribe((result) => {
	      if (result.status === "revealed") {
	        if (this.#heldVisiblePostNumber !== null) {
	          this.#commitCachedSources(
	            this.#heldVisiblePostNumber,
	            this.#snapshot.pendingPostNumber
	          );
	          return;
	        }
	        this.#commit(
	          result.rootPostNumber ?? result.postNumber,
	          this.#snapshot.pendingPostNumber
	        );
	      }
	    }, this.scope), this.scope.add(() => {
	      this.#jumpEpoch += 1, this.#clearVisiblePostHold(), this.changes.clear();
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  refresh() {
	    return this.#commit(
	      this.#snapshot.currentPostNumber,
	      this.#snapshot.pendingPostNumber
	    );
	  }
	  /**
	   * 历史恢复会在虚拟窗口补齐、测量和锚点补偿期间连续产生程序化 scroll。
	   * 在用户真正发出滚动意图前固定历史楼层,避免这些布局事件把时间线和宿主
	   * Topic 列表的“定位”字段改写成相邻物理窗口。返回值只释放本次 generation,
	   * 旧恢复任务不得误解锁同楼层的新恢复。
	   */
	  holdVisiblePost(postNumber) {
	    if (this.scope.destroyed) return () => {
	    };
	    const target = clampedPostNumber(
	      postNumber,
	      this.#snapshot.totalPostCount
	    ), generation = ++this.#visiblePostHoldGeneration;
	    this.#heldVisiblePostNumber = target, this.#commitCachedSources(target, this.#snapshot.pendingPostNumber);
	    let active = !0;
	    return () => {
	      active && (active = !1, generation === this.#visiblePostHoldGeneration && this.#clearVisiblePostHold());
	    };
	  }
	  syncVisiblePost(postNumber, options = {}) {
	    if (this.#heldVisiblePostNumber !== null)
	      return this.#commitCachedSources(
	        this.#heldVisiblePostNumber,
	        this.#snapshot.pendingPostNumber
	      );
	    const navigable = this.#snapshot.navigablePostNumbers, boundaryPostNumber = options.atStart ? navigable?.[0] ?? 1 : options.atEnd ? navigable?.at(-1) ?? this.#snapshot.totalPostCount : postNumber;
	    return this.#commitCachedSources(
	      boundaryPostNumber,
	      this.#snapshot.pendingPostNumber
	    );
	  }
	  progressFor(postNumber) {
	    const target = clampedPostNumber(postNumber, this.#snapshot.totalPostCount), posts = this.#snapshot.navigablePostNumbers;
	    if (posts) {
	      const index = posts.indexOf(target);
	      if (index >= 0) return posts.length > 1 ? index / (posts.length - 1) : 0;
	    }
	    return this.#snapshot.totalPostCount > 1 ? (target - 1) / (this.#snapshot.totalPostCount - 1) : 0;
	  }
	  targetAtRatio(ratio) {
	    const normalizedRatio = Number.isFinite(ratio) ? Math.max(0, Math.min(1, ratio)) : 0, posts = this.#snapshot.navigablePostNumbers;
	    return posts ? posts[Math.round(normalizedRatio * (posts.length - 1))] : clampedPostNumber(
	      Math.round(1 + normalizedRatio * (this.#snapshot.totalPostCount - 1)),
	      this.#snapshot.totalPostCount
	    );
	  }
	  targetAtEnd() {
	    return clampedPostNumber(
	      this.#snapshot.totalPostCount,
	      this.#snapshot.totalPostCount
	    );
	  }
	  targetByStep(currentPostNumber, delta) {
	    const posts = this.#snapshot.navigablePostNumbers;
	    if (posts) {
	      const current = (0, import_identifiers.tryDiscoursePostNumber)(currentPostNumber), currentIndex = current === null ? -1 : posts.indexOf(current), baseIndex = currentIndex >= 0 ? currentIndex : 0, nextIndex = Number.isFinite(delta) ? Math.max(
	        0,
	        Math.min(posts.length - 1, baseIndex + Math.trunc(delta))
	      ) : delta < 0 ? 0 : posts.length - 1;
	      return posts[nextIndex];
	    }
	    const numericDelta = Number.isFinite(delta) ? Math.trunc(delta) : delta < 0 ? -this.#snapshot.totalPostCount : this.#snapshot.totalPostCount;
	    return clampedPostNumber(
	      currentPostNumber + numericDelta,
	      this.#snapshot.totalPostCount
	    );
	  }
	  validateInput(rawValue) {
	    const total = this.#snapshot.totalPostCount;
	    if (!rawValue)
	      return Object.freeze({
	        postNumber: null,
	        message: `请输入楼层号(1–${total})`
	      });
	    if (!/^\d+$/.test(rawValue))
	      return Object.freeze({
	        postNumber: null,
	        message: "仅支持十进制整数"
	      });
	    const value = Number(rawValue);
	    return Number.isSafeInteger(value) ? value < 1 || value > total ? Object.freeze({
	      postNumber: null,
	      message: `超出范围,请输入 1–${total}`
	    }) : Object.freeze({
	      postNumber: (0, import_identifiers.discoursePostReference)({ post_number: value }).postNumber,
	      message: ""
	    }) : Object.freeze({
	      postNumber: null,
	      message: "楼层号数值过大"
	    });
	  }
	  async jumpTo(postNumber, options = {}) {
	    if (this.scope.destroyed)
	      throw new Error("ReaderTopicTimelineController 已销毁");
	    this.#clearVisiblePostHold();
	    const target = (0, import_identifiers.discoursePostReference)({ post_number: postNumber }).postNumber;
	    if (target > this.#snapshot.totalPostCount)
	      throw new RangeError(`目标楼层超出范围:1–${this.#snapshot.totalPostCount}`);
	    const epoch = ++this.#jumpEpoch;
	    this.#commit(this.#snapshot.currentPostNumber, target);
	    try {
	      return await this.#navigation.navigate({
	        postNumber: target,
	        source: "timeline",
	        ...options.alignment === void 0 ? {} : { alignment: options.alignment },
	        ...options.focus === void 0 ? {} : { focus: options.focus },
	        ...options.highlight === void 0 ? {} : { highlight: options.highlight }
	      });
	    } finally {
	      epoch === this.#jumpEpoch && !this.scope.destroyed && this.#commit(this.#snapshot.currentPostNumber, null);
	    }
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #derive(currentPostNumber, pendingPostNumber) {
	    const totalPostCount = normalizedTotal(this.#readTotalPostCount()), navigablePostNumbers = this.#readNavigablePostNumbersComplete() ? normalizedNavigablePosts(
	      this.#readNavigablePostNumbers(),
	      totalPostCount
	    ) : null;
	    return this.#deriveFromSources(
	      currentPostNumber,
	      pendingPostNumber,
	      totalPostCount,
	      navigablePostNumbers
	    );
	  }
	  #deriveFromSources(currentPostNumber, pendingPostNumber, totalPostCount, navigablePostNumbers) {
	    const current = clampedPostNumber(currentPostNumber, totalPostCount), progress = navigablePostNumbers?.includes(current) ? navigablePostNumbers.length > 1 ? navigablePostNumbers.indexOf(current) / (navigablePostNumbers.length - 1) : 0 : totalPostCount > 1 ? (current - 1) / (totalPostCount - 1) : 0;
	    return Object.freeze({
	      currentPostNumber: current,
	      totalPostCount,
	      progress,
	      pendingPostNumber: pendingPostNumber !== null && pendingPostNumber <= totalPostCount ? pendingPostNumber : null,
	      navigablePostNumbers
	    });
	  }
	  #commitCachedSources(currentPostNumber, pendingPostNumber) {
	    return this.#accept(this.#deriveFromSources(
	      currentPostNumber,
	      pendingPostNumber,
	      this.#snapshot.totalPostCount,
	      this.#snapshot.navigablePostNumbers
	    ));
	  }
	  #commit(currentPostNumber, pendingPostNumber) {
	    return this.#accept(this.#derive(currentPostNumber, pendingPostNumber));
	  }
	  #accept(next) {
	    if (snapshotsEqual(this.#snapshot, next)) return this.#snapshot;
	    this.#snapshot = next;
	    for (const error of this.changes.emit(next)) this.#onError(error);
	    return next;
	  }
	  #clearVisiblePostHold() {
	    this.#heldVisiblePostNumber = null, this.#visiblePostHoldGeneration += 1;
	  }
	}
}, "b094b21e3324fe9a4286353886e51837cda01d1f786f4143fd5ff18db4b0d37e");

/* Source: lite/src/topic/reader-topic-timeline-view.ts */
runtime.register("src/topic/reader-topic-timeline-view.js", function(module, exports, require) {
	var reader_topic_timeline_view_exports = {};
	__export(reader_topic_timeline_view_exports, {
	  ReaderTopicTimelineView: () => ReaderTopicTimelineView,
	  readerTimelineDateLabel: () => readerTimelineDateLabel
	});
	module.exports = __toCommonJS(reader_topic_timeline_view_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	function normalizedPreferences(value) {
	  const numeric = Math.floor(Number(value.pageStep));
	  return Object.freeze({
	    pageStep: Number.isSafeInteger(numeric) && numeric > 0 ? Math.min(64, numeric) : 16
	  });
	}
	function normalizedInset(value) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric >= 0 ? numeric : 10;
	}
	function readerTimelineDateLabel(timestamp) {
	  if (!timestamp) return "";
	  const date = new Date(timestamp);
	  return Number.isFinite(date.getTime()) ? `${date.getFullYear()} 年
${date.getMonth() + 1} 月 ${date.getDate()} 日` : "";
	}
	function pointerCoordinate(event) {
	  const value = Number(event.clientY);
	  return Number.isFinite(value) ? value : 0;
	}
	function pointerIdentifier(event) {
	  const value = Number(event.pointerId);
	  return Number.isFinite(value) ? value : 0;
	}
	function eventPathIncludes(event, node) {
	  const composedPath = event.composedPath;
	  if (typeof composedPath == "function")
	    return composedPath.call(event).includes(node);
	  const target = event.target;
	  return target !== null && typeof target == "object" && "nodeType" in target && node.contains(target);
	}
	class ReaderTopicTimelineView {
	  scope;
	  #controller;
	  #elements;
	  #readCreatedAt;
	  #readLatestReplyAt;
	  #formatRelative;
	  #frameScheduler;
	  #animationFrameScheduler;
	  #scheduleTimer;
	  #cancelTimer;
	  #now;
	  #prefersReducedMotion;
	  #trackTopInset;
	  #trackBottomInset;
	  #notify;
	  #onError;
	  #lensElements = /* @__PURE__ */ new Map();
	  #spareLensElements = [];
	  #preferences;
	  #previewFrame = 0;
	  #jumpAnimationFrame = 0;
	  #jumpAnimationTimer = 0;
	  #jumpAnimationEpoch = 0;
	  #previewClientY = 0;
	  #previewVisible = !1;
	  #pointerInside = !1;
	  #dragging = !1;
	  #activePointerId = 0;
	  #trackRect = null;
	  constructor(options) {
	    this.#controller = options.controller, this.#elements = options.elements, this.#readCreatedAt = options.readCreatedAt, this.#readLatestReplyAt = options.readLatestReplyAt, this.#formatRelative = options.formatRelative, this.#trackTopInset = normalizedInset(options.trackTopInset), this.#trackBottomInset = normalizedInset(options.trackBottomInset), this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.#preferences = normalizedPreferences(options.preferences);
	    const view = this.#elements.timeline.ownerDocument.defaultView;
	    this.#frameScheduler = options.frameScheduler ?? Object.freeze({
	      request(callback) {
	        return typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(callback) : (callback(0), 0);
	      },
	      cancel(frameId) {
	        view?.cancelAnimationFrame?.(frameId);
	      }
	    }), this.#animationFrameScheduler = options.animationFrameScheduler ?? Object.freeze({
	      request(callback) {
	        return typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(callback) : view?.setTimeout(
	          () => callback(Date.now()),
	          16
	        ) ?? globalThis.setTimeout(
	          () => callback(Date.now()),
	          16
	        );
	      },
	      cancel(frameId) {
	        typeof view?.cancelAnimationFrame == "function" ? view.cancelAnimationFrame(frameId) : view ? view.clearTimeout(frameId) : globalThis.clearTimeout(frameId);
	      }
	    }), this.#scheduleTimer = options.scheduleTimer ?? ((callback, delayMs) => view?.setTimeout(callback, delayMs) ?? globalThis.setTimeout(callback, delayMs)), this.#cancelTimer = options.cancelTimer ?? ((timerId) => {
	      view ? view.clearTimeout(timerId) : globalThis.clearTimeout(timerId);
	    }), this.#now = options.now ?? Date.now, this.#prefersReducedMotion = options.prefersReducedMotion ?? (() => view?.matchMedia?.("(prefers-reduced-motion: reduce)").matches === !0), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const timelineDocument = this.#elements.timeline.ownerDocument, timelineWindow = timelineDocument.defaultView;
	    let relativeTimer = null;
	    const stopRelativeTimer = () => {
	      relativeTimer !== null && (timelineWindow?.clearInterval(relativeTimer), relativeTimer = null);
	    }, syncRelativeTimer = () => {
	      stopRelativeTimer(), !(!timelineWindow || timelineDocument.visibilityState === "hidden") && (relativeTimer = timelineWindow.setInterval(() => {
	        this.scope.destroyed || this.#syncRelativeTime();
	      }, 3e4));
	    };
	    this.#listen(
	      timelineDocument,
	      "visibilitychange",
	      syncRelativeTimer
	    ), this.scope.add(stopRelativeTimer), syncRelativeTimer();
	    const {
	      root,
	      date,
	      track,
	      relative,
	      jump,
	      top,
	      jumpForm,
	      jumpInput
	    } = this.#elements;
	    this.#listen(date, "click", () => {
	      this.#submitJump(1);
	    }), this.#listen(top, "click", () => {
	      this.#submitJump(1);
	    }), this.#listen(relative, "click", () => {
	      const target = this.#controller.targetAtEnd();
	      this.#submitJump(target, !1);
	    }), this.#listen(jump, "click", () => {
	      jumpForm.hidden ? this.#openJumpForm() : this.#closeJumpForm(!0);
	    }), this.#listen(jumpInput, "input", () => {
	      this.#validateJumpInput();
	    }), this.#listen(jumpForm, "submit", (event) => {
	      event.preventDefault();
	      const validation = this.#validateJumpInput();
	      validation !== null && (this.#closeJumpForm(), this.#submitJump(validation));
	    }), this.#listen(jumpForm, "keydown", (event) => {
	      const key = event.key;
	      key === "Enter" && this.#elements.jumpSubmit.disabled ? (event.preventDefault(), event.stopPropagation()) : key === "Escape" && (event.preventDefault(), event.stopPropagation(), this.#closeJumpForm(!0));
	    }), this.#listen(root.getRootNode(), "pointerdown", (event) => {
	      !jumpForm.hidden && !eventPathIncludes(event, jumpForm) && !eventPathIncludes(event, jump) && this.#closeJumpForm();
	    }, !0), this.#listen(track, "pointerenter", (event) => {
	      this.#pointerInside = !0, this.#refreshTrackRect(), this.#showPointerPreview(pointerCoordinate(event));
	    }), this.#listen(track, "pointerleave", () => {
	      this.#pointerInside = !1, this.#dragging || this.#hidePointerPreview();
	    }), this.#listen(track, "pointerdown", (event) => {
	      this.#dragging || (event.preventDefault(), this.#dragging = !0, this.#activePointerId = pointerIdentifier(event), this.#refreshTrackRect(), this.#capturePointer(this.#activePointerId), this.#showPointerPreview(pointerCoordinate(event)));
	    }), this.#listen(track, "pointermove", (event) => {
	      this.#previewVisible && this.#showPointerPreview(pointerCoordinate(event));
	    }), this.#listen(track, "pointerup", (event) => {
	      const pointerId = pointerIdentifier(event);
	      if (!this.#dragging || pointerId !== this.#activePointerId) return;
	      this.#dragging = !1, this.#releasePointer(pointerId), this.#activePointerId = 0;
	      const target = this.#controller.targetAtRatio(
	        this.#pointerPosition(pointerCoordinate(event)).ratio
	      );
	      this.#pointerInside || this.#hidePointerPreview(), this.#submitJump(target);
	    }), this.#listen(track, "pointercancel", (event) => {
	      !this.#dragging || pointerIdentifier(event) !== this.#activePointerId || (this.#dragging = !1, this.#releasePointer(this.#activePointerId), this.#activePointerId = 0, this.#hidePointerPreview(), this.#sync(this.#controller.snapshot));
	    }), this.#listen(track, "keydown", (event) => {
	      this.#onTrackKeyDown(event);
	    }), this.#controller.changes.subscribe((snapshot) => {
	      this.#sync(snapshot);
	    }, this.scope), this.scope.add(() => {
	      this.#finishJumpAnimation(!1), this.#elements.track.classList.remove("ldp-timeline-pending"), this.#previewFrame && (this.#frameScheduler.cancel(this.#previewFrame), this.#previewFrame = 0), this.#releasePointer(this.#activePointerId), this.#closeJumpForm(), this.#hidePointerPreview(), this.#elements.cursor.replaceChildren(), this.#lensElements.clear(), this.#spareLensElements.length = 0, this.#elements.timeline.hidden = !0;
	    }), this.#sync(this.#controller.snapshot);
	  }
	  applyPreferences(value) {
	    this.#assertActive(), this.#preferences = normalizedPreferences(value);
	  }
	  refresh() {
	    this.#assertActive(), this.#sync(this.#controller.snapshot);
	  }
	  focusJump() {
	    this.#assertActive(), this.#openJumpForm();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #sync(snapshot) {
	    const {
	      timeline,
	      date,
	      track,
	      current,
	      total,
	      jump,
	      top,
	      jumpInput
	    } = this.#elements, hidden = snapshot.totalPostCount <= 1;
	    timeline.hidden !== hidden && (timeline.hidden = hidden);
	    const progress = String(snapshot.progress);
	    timeline.style.getPropertyValue("--ldp-timeline-progress") !== progress && timeline.style.setProperty("--ldp-timeline-progress", progress), this.#setText(current, String(snapshot.currentPostNumber)), this.#setText(total, String(snapshot.totalPostCount)), this.#setAttribute(track, "aria-valuemin", "1"), this.#setAttribute(track, "aria-valuemax", String(snapshot.totalPostCount)), this.#setAttribute(track, "aria-valuenow", String(snapshot.currentPostNumber)), this.#setAttribute(
	      track,
	      "aria-valuetext",
	      `第 ${snapshot.currentPostNumber} 楼,共 ${snapshot.totalPostCount} 楼`
	    ), this.#setAttribute(
	      jump,
	      "aria-label",
	      `跳到指定楼层,当前第 ${snapshot.currentPostNumber} 楼,共 ${snapshot.totalPostCount} 楼`
	    );
	    const maxLength = String(snapshot.totalPostCount).length + 1;
	    jumpInput.maxLength !== maxLength && (jumpInput.maxLength = maxLength), this.#setAttribute(
	      jumpInput,
	      "aria-label",
	      `楼层号,范围 1 到 ${snapshot.totalPostCount}`
	    );
	    const createdAt = this.#readCreatedAt(snapshot.currentPostNumber), dateText = readerTimelineDateLabel(createdAt);
	    this.#setText(date, dateText);
	    const pending = snapshot.pendingPostNumber !== null;
	    this.#syncRelativeTime(pending), track.classList.contains("ldp-timeline-pending") !== pending && track.classList.toggle("ldp-timeline-pending", pending), this.#setAttribute(timeline, "aria-busy", String(pending)), this.#setDisabled(track, pending), this.#setDisabled(date, pending || !dateText), this.#setDisabled(jump, pending), this.#setDisabled(top, pending), this.#setDisabled(jumpInput, pending), this.#elements.jumpForm.hidden || this.#validateJumpInput();
	  }
	  #syncRelativeTime(pending = this.#controller.snapshot.pendingPostNumber !== null) {
	    const latestReplyAt = this.#readLatestReplyAt() ?? "";
	    let relativeText = "";
	    if (latestReplyAt)
	      try {
	        relativeText = this.#formatRelative(latestReplyAt);
	      } catch (error) {
	        this.#report(error);
	      }
	    this.#setText(this.#elements.relative, relativeText), this.#setDisabled(
	      this.#elements.relative,
	      pending || !relativeText
	    );
	  }
	  #setAttribute(element, name, value) {
	    element.getAttribute(name) !== value && element.setAttribute(name, value);
	  }
	  #setText(element, value) {
	    element.textContent !== value && (element.textContent = value);
	  }
	  #setDisabled(element, disabled) {
	    element.disabled !== disabled && (element.disabled = disabled);
	  }
	  #onTrackKeyDown(event) {
	    const current = this.#controller.snapshot.currentPostNumber;
	    let delta;
	    if (event.key === "ArrowUp" || event.key === "ArrowLeft") delta = -1;
	    else if (event.key === "ArrowDown" || event.key === "ArrowRight") delta = 1;
	    else if (event.key === "PageUp") delta = -this.#preferences.pageStep;
	    else if (event.key === "PageDown") delta = this.#preferences.pageStep;
	    else if (event.key === "Home") delta = Number.NEGATIVE_INFINITY;
	    else if (event.key === "End") delta = Number.POSITIVE_INFINITY;
	    else return;
	    event.preventDefault(), this.#submitJump(this.#controller.targetByStep(current, delta));
	  }
	  #openJumpForm() {
	    this.#hidePointerPreview();
	    const { jump, jumpForm, jumpInput } = this.#elements;
	    jumpInput.value = String(this.#controller.snapshot.currentPostNumber), jumpForm.hidden = !1, jump.setAttribute("aria-expanded", "true"), this.#validateJumpInput(), jumpInput.focus(), jumpInput.select?.();
	  }
	  #closeJumpForm(restoreFocus = !1) {
	    const { jump, jumpForm } = this.#elements;
	    jumpForm.hidden || (jumpForm.hidden = !0, jump.setAttribute("aria-expanded", "false"), restoreFocus && jump.focus());
	  }
	  #validateJumpInput() {
	    const {
	      jumpForm,
	      jumpInput,
	      jumpSubmit,
	      jumpHint
	    } = this.#elements, validation = this.#controller.validateInput(jumpInput.value), invalid = validation.postNumber === null;
	    return jumpInput.setAttribute("aria-invalid", String(invalid)), jumpForm.classList.toggle("is-invalid", invalid), jumpHint.textContent = invalid ? validation.message : `有效范围:1–${this.#controller.snapshot.totalPostCount}`, jumpSubmit.disabled = invalid || this.#controller.snapshot.pendingPostNumber !== null, validation.postNumber;
	  }
	  #submitJump(postNumber, animate = !0) {
	    this.scope.destroyed || this.#controller.snapshot.pendingPostNumber !== null || (animate ? this.#beginJumpAnimation(postNumber) : this.#finishJumpAnimation(!1), this.#controller.jumpTo(postNumber).then((result) => {
	      this.scope.destroyed || result.status !== "revealed" && result.status !== "superseded" && this.#notify(`暂时无法定位到楼层 #${postNumber},可重试`);
	    }).catch((error) => {
	      this.scope.destroyed || (this.#report(error), this.#notify(`楼层 #${postNumber} 加载失败,请重试`));
	    }));
	  }
	  #beginJumpAnimation(postNumber) {
	    if (this.#finishJumpAnimation(!1), this.#prefersReducedMotion() || this.scope.destroyed) return;
	    const snapshot = this.#controller.snapshot, currentRatio = this.#ratioForPost(
	      snapshot.currentPostNumber,
	      snapshot
	    ), targetRatio = this.#ratioForPost(postNumber, snapshot), startRatio = this.#previewVisible && this.#lensElements.get(postNumber)?.classList.contains(
	      "ldp-timeline-lens-selected"
	    ) === !0 ? targetRatio : currentRatio;
	    this.#renderLens(startRatio, this.#topForRatio(startRatio));
	    const { track, cursor } = this.#elements;
	    track.classList.remove("ldp-timeline-jumping"), cursor.offsetWidth, track.classList.add("ldp-timeline-jumping");
	    const epoch = ++this.#jumpAnimationEpoch, durationMs = 360;
	    let startedAt = null;
	    const settle = () => {
	      this.scope.destroyed || epoch !== this.#jumpAnimationEpoch || (this.#jumpAnimationFrame = 0, this.#renderLens(targetRatio, this.#topForRatio(targetRatio)), this.#jumpAnimationTimer = this.#scheduleTimer(() => {
	        epoch === this.#jumpAnimationEpoch && this.#finishJumpAnimation(!0);
	      }, 460));
	    };
	    if (Math.abs(targetRatio - startRatio) <= 1e-4) {
	      this.#jumpAnimationFrame = this.#animationFrameScheduler.request(settle);
	      return;
	    }
	    const animate = (timestamp) => {
	      if (this.scope.destroyed || epoch !== this.#jumpAnimationEpoch) return;
	      const currentTime = Number.isFinite(timestamp) ? timestamp : this.#now();
	      startedAt === null && (startedAt = currentTime);
	      const elapsedRatio = Math.min(
	        1,
	        Math.max(0, (currentTime - startedAt) / durationMs)
	      ), easedRatio = 1 - Math.pow(1 - elapsedRatio, 3), ratio = startRatio + (targetRatio - startRatio) * easedRatio;
	      if (this.#renderLens(ratio, this.#topForRatio(ratio)), elapsedRatio >= 1) {
	        settle();
	        return;
	      }
	      this.#jumpAnimationFrame = this.#animationFrameScheduler.request(animate);
	    };
	    this.#jumpAnimationFrame = this.#animationFrameScheduler.request(animate);
	  }
	  #finishJumpAnimation(resumePreview) {
	    this.#jumpAnimationEpoch += 1, this.#jumpAnimationFrame && (this.#animationFrameScheduler.cancel(this.#jumpAnimationFrame), this.#jumpAnimationFrame = 0), this.#jumpAnimationTimer && (this.#cancelTimer(this.#jumpAnimationTimer), this.#jumpAnimationTimer = 0), this.#elements.track.classList.remove("ldp-timeline-jumping"), resumePreview && this.#previewVisible && this.#showPointerPreview(this.#previewClientY);
	  }
	  #ratioForPost(postNumber, snapshot) {
	    const navigable = snapshot.navigablePostNumbers;
	    if (navigable?.length) {
	      const index = navigable.findIndex(
	        (candidate) => Number(candidate) === postNumber
	      );
	      if (index >= 0) return navigable.length === 1 ? 0 : index / (navigable.length - 1);
	    }
	    return snapshot.totalPostCount <= 1 ? 0 : Math.max(0, Math.min(
	      1,
	      (postNumber - 1) / (snapshot.totalPostCount - 1)
	    ));
	  }
	  #topForRatio(ratio) {
	    const rect = this.#trackRect ?? this.#refreshTrackRect(), usableHeight = Math.max(
	      1,
	      rect.height - this.#trackTopInset - this.#trackBottomInset
	    );
	    return this.#trackTopInset + Math.max(0, Math.min(1, ratio)) * usableHeight;
	  }
	  #showPointerPreview(clientY) {
	    this.#previewVisible = !0, this.#previewClientY = clientY, !this.#previewFrame && (this.#previewFrame = this.#frameScheduler.request(() => {
	      if (this.#previewFrame = 0, !this.#previewVisible || this.scope.destroyed) return;
	      const position = this.#pointerPosition(this.#previewClientY), target = this.#controller.targetAtRatio(position.ratio);
	      this.#elements.timeline.style.setProperty(
	        "--ldp-timeline-preview-progress",
	        String(position.ratio)
	      ), this.#elements.preview.textContent = `#${target}`, this.#elements.track.classList.toggle(
	        "ldp-timeline-previewing",
	        target !== this.#controller.snapshot.currentPostNumber
	      ), this.#elements.track.classList.add("ldp-timeline-hovering"), this.#renderLens(position.ratio, position.top);
	    }));
	  }
	  #hidePointerPreview() {
	    this.#previewVisible = !1, this.#previewFrame && (this.#frameScheduler.cancel(this.#previewFrame), this.#previewFrame = 0), this.#trackRect = null, this.#elements.track.classList.remove(
	      "ldp-timeline-previewing",
	      "ldp-timeline-hovering"
	    );
	  }
	  #renderLens(ratio, top) {
	    const snapshot = this.#controller.snapshot, navigablePosts = snapshot.navigablePostNumbers, floorCount = navigablePosts?.length ?? snapshot.totalPostCount, floorAt = (index) => navigablePosts?.[index] ?? index + 1, continuousIndex = Math.max(
	      0,
	      Math.min(floorCount - 1, ratio * (floorCount - 1))
	    ), nearestIndex = Math.round(continuousIndex), selectedFloor = floorAt(nearestIndex), firstIndex = Math.max(0, nearestIndex - 3), lastIndex = Math.min(floorCount - 1, nearestIndex + 3), desired = /* @__PURE__ */ new Map();
	    for (let index = firstIndex; index <= lastIndex; index += 1)
	      desired.set(floorAt(index), index - continuousIndex);
	    for (const [floor, element] of this.#lensElements)
	      desired.has(floor) || (element.hidden = !0, this.#lensElements.delete(floor), this.#spareLensElements.push(element));
	    for (const [floor, offset] of desired) {
	      let element = this.#lensElements.get(floor);
	      element || (element = this.#spareLensElements.pop() ?? this.#elements.timeline.ownerDocument.createElement("span"), element.textContent = `#${floor}`, element.hidden = !1, this.#lensElements.set(floor, element), element.isConnected || this.#elements.cursor.append(element));
	      const distance = Math.abs(offset), focus = Math.exp(-0.72 * distance * distance), opacity = Math.pow(Math.max(0, 1 - distance / 3), 1.25), scale = 0.46 + 0.54 * focus, shiftX = -Math.min(6, distance * 2);
	      element.style.setProperty("--ldp-lens-offset", String(offset)), element.style.setProperty("--ldp-lens-scale", scale.toFixed(4)), element.style.setProperty("--ldp-lens-opacity", opacity.toFixed(4)), element.style.setProperty(
	        "--ldp-lens-shift-x",
	        `${shiftX.toFixed(2)}px`
	      ), element.classList.toggle(
	        "ldp-timeline-lens-selected",
	        floor === selectedFloor
	      ), element.classList.toggle(
	        "ldp-timeline-lens-focus",
	        Math.abs(offset) < 1e-3
	      );
	    }
	    this.#elements.cursor.style.setProperty(
	      "--ldp-timeline-lens-top",
	      `${top.toFixed(2)}px`
	    );
	  }
	  #pointerPosition(clientY) {
	    const rect = this.#trackRect ?? this.#refreshTrackRect(), usableHeight = Math.max(
	      1,
	      rect.height - this.#trackTopInset - this.#trackBottomInset
	    ), localTop = Math.max(
	      this.#trackTopInset,
	      Math.min(
	        Math.max(this.#trackTopInset, rect.height - this.#trackBottomInset),
	        clientY - rect.top
	      )
	    );
	    return Object.freeze({
	      top: localTop,
	      ratio: Math.max(
	        0,
	        Math.min(1, (localTop - this.#trackTopInset) / usableHeight)
	      )
	    });
	  }
	  #refreshTrackRect() {
	    return this.#trackRect = this.#elements.track.getBoundingClientRect(), this.#trackRect;
	  }
	  #capturePointer(pointerId) {
	    const track = this.#elements.track;
	    if (!(!pointerId || typeof track.setPointerCapture != "function"))
	      try {
	        track.setPointerCapture(pointerId);
	      } catch {
	      }
	  }
	  #releasePointer(pointerId) {
	    const track = this.#elements.track;
	    if (!(!pointerId || typeof track.releasePointerCapture != "function"))
	      try {
	        (typeof track.hasPointerCapture != "function" || track.hasPointerCapture(pointerId)) && track.releasePointerCapture(pointerId);
	      } catch {
	      }
	  }
	  #listen(target, type, listener, options) {
	    this.scope.listen(target, type, listener, options);
	  }
	  #report(error) {
	    try {
	      this.#onError(error);
	    } catch {
	    }
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderTopicTimelineView 已销毁");
	  }
	}
}, "d43608f0d25f4f22b459437dd779e1d7ecb93d83ec75eec95e5bffd966905cd2");

/* Source: lite/src/topic/topic-read-request-adapter.ts */
runtime.register("src/topic/topic-read-request-adapter.js", function(module, exports, require) {
	var topic_read_request_adapter_exports = {};
	__export(topic_read_request_adapter_exports, {
	  TopicReadRequestAdapter: () => TopicReadRequestAdapter
	});
	module.exports = __toCommonJS(topic_read_request_adapter_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_native_request_descriptors = require("../discourse/native-request-descriptors.js");
	function cacheWithPostIds(cache, postIds) {
	  return Object.freeze({
	    ...cache,
	    tags: Object.freeze(
	      [.../* @__PURE__ */ new Set([
	        ...cache.tags,
	        ...postIds.map((postId) => `post:${(0, import_identifiers.discoursePostId)(postId)}`)
	      ])].sort()
	    )
	  });
	}
	function cacheForTopic(cache, topicId) {
	  return Object.freeze({
	    ...cache,
	    tags: Object.freeze([
	      ...cache.tags.filter((tag) => !tag.startsWith("topic:")),
	      `topic:${topicId}`
	    ])
	  });
	}
	function linkedRequestSignal(topicSignal, requestSignal) {
	  if (!requestSignal || requestSignal === topicSignal)
	    return Object.freeze({ signal: topicSignal, dispose() {
	    } });
	  const controller = new AbortController(), abortFromTopic = () => controller.abort(topicSignal.reason), abortFromRequest = () => controller.abort(requestSignal.reason);
	  return topicSignal.aborted ? abortFromTopic() : requestSignal.aborted ? abortFromRequest() : (topicSignal.addEventListener("abort", abortFromTopic, { once: !0 }), requestSignal.addEventListener("abort", abortFromRequest, { once: !0 })), Object.freeze({
	    signal: controller.signal,
	    dispose() {
	      topicSignal.removeEventListener("abort", abortFromTopic), requestSignal.removeEventListener("abort", abortFromRequest);
	    }
	  });
	}
	class TopicReadRequestAdapter {
	  topicId;
	  authScope;
	  #gateway;
	  #transport;
	  #signal;
	  #caches;
	  #basePath;
	  constructor(options) {
	    this.#gateway = options.gateway, this.#transport = options.transport, this.authScope = (0, import_identifiers.discourseAuthScope)(options.authScope), this.topicId = (0, import_identifiers.discourseTopicId)(options.topicId), this.#signal = options.signal, this.#caches = options.caches, this.#basePath = (0, import_native_request_descriptors.discourseBasePath)(options.basePath);
	  }
	  loadTopic(options = {}) {
	    const descriptor = import_native_request_descriptors.DiscourseNativeRequests.topic({
	      basePath: this.#basePath,
	      topicId: this.topicId
	    });
	    return this.#gateway.loadTopicTarget({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      operation: "topic-refresh",
	      profile: options.background ? options.prefetchTier === "nearby" ? "nearby-prefetch" : "background-prefetch" : "topic-visible",
	      input: descriptor.path,
	      signal: this.#signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: options.refresh === !0 ? "refresh" : "default",
	      cache: this.#caches.topic,
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    });
	  }
	  loadPostsByIds(rawPostIds, options = {}) {
	    const postIds = (0, import_identifiers.discoursePostIds)(rawPostIds), descriptor = import_native_request_descriptors.DiscourseNativeRequests.postsById({
	      basePath: this.#basePath,
	      topicId: this.topicId,
	      postIds
	    });
	    return this.#gateway.loadTopicPosts({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      postIds,
	      profile: options.background ? options.prefetchTier === "nearby" ? "nearby-prefetch" : "background-prefetch" : options.priority === "nested" ? "nested-visible" : "topic-visible",
	      input: descriptor.path,
	      signal: this.#signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: options.refresh ? "refresh" : "default",
	      cache: cacheWithPostIds(this.#caches.posts, postIds),
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    });
	  }
	  promotePostsByIds(rawPostIds, options = {}) {
	    const postIds = (0, import_identifiers.discoursePostIds)(rawPostIds);
	    return this.#gateway.promoteTopicPosts?.({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      postIds,
	      profile: options.background ? options.prefetchTier === "nearby" ? "nearby-prefetch" : "background-prefetch" : options.priority === "nested" ? "nested-visible" : "topic-visible",
	      cacheMode: options.refresh ? "refresh" : "default"
	    }) ?? !1;
	  }
	  loadPostById(rawPostId, options = {}) {
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId), descriptor = import_native_request_descriptors.DiscourseNativeRequests.postById({
	      basePath: this.#basePath,
	      postId
	    });
	    return this.#gateway.loadTopicTarget({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      operation: "post-by-id-refresh",
	      postId,
	      profile: options.background ? "background-prefetch" : "topic-visible",
	      input: descriptor.path,
	      signal: this.#signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: "refresh",
	      cache: cacheWithPostIds(this.#caches.posts, [postId]),
	      allowStaleOnError: !1,
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    });
	  }
	  loadPostVotingComments(rawPostId, options = {}) {
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId), afterCommentId = Number(options.afterCommentId ?? 0);
	    if (!Number.isSafeInteger(afterCommentId) || afterCommentId < 0)
	      throw new RangeError("afterCommentId 必须是非负安全整数");
	    const descriptor = import_native_request_descriptors.DiscourseNativeRequests.postVotingComments({
	      basePath: this.#basePath,
	      postId,
	      afterCommentId
	    });
	    return this.#gateway.loadCollectionPage({
	      authScope: this.authScope,
	      collection: `post-voting-comments:${postId}`,
	      page: afterCommentId,
	      cursor: afterCommentId,
	      profile: options.background ? "background-prefetch" : "collection-visible",
	      input: descriptor.path,
	      signal: this.#signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: options.refresh ? "refresh" : "default",
	      cache: cacheWithPostIds(this.#caches.posts, [postId]),
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    });
	  }
	  targetCandidates(rawPostNumber, options, rawTopicId = this.topicId) {
	    const postNumber = (0, import_identifiers.discoursePostNumber)(rawPostNumber), topicId = (0, import_identifiers.discourseTopicId)(rawTopicId);
	    return Object.freeze(import_native_request_descriptors.DiscourseNativeRequests.targetCandidates({
	      basePath: this.#basePath,
	      topicId,
	      postNumber,
	      scope: options.scope,
	      ...options.slug === void 0 ? {} : { slug: options.slug },
	      ...options.refresh === void 0 ? {} : { refresh: options.refresh }
	    }).map(({ endpoint, url }) => Object.freeze({ endpoint, url })));
	  }
	  loadTargetCandidate(candidate, rawPostNumber, options, rawTopicId = this.topicId) {
	    const postNumber = (0, import_identifiers.discoursePostNumber)(rawPostNumber), topicId = (0, import_identifiers.discourseTopicId)(rawTopicId), refresh = options.refresh === !0, catalogCandidate = import_native_request_descriptors.DiscourseNativeRequests.targetCandidates({
	      basePath: this.#basePath,
	      topicId,
	      postNumber,
	      scope: options.scope,
	      ...options.slug === void 0 ? {} : { slug: options.slug },
	      refresh
	    }).find((entry) => entry.endpoint === candidate.endpoint && entry.url === candidate.url);
	    if (!catalogCandidate)
	      throw new Error("目标楼层请求不属于 Discourse 原生目录");
	    const descriptor = catalogCandidate.descriptor;
	    return this.#gateway.loadTopicTarget({
	      authScope: this.authScope,
	      topicId,
	      operation: `target:${options.scope}:${candidate.endpoint}`,
	      postNumber,
	      profile: options.background ? "background-prefetch" : "topic-visible",
	      input: candidate.url,
	      signal: this.#signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: refresh ? "refresh" : "default",
	      cache: cacheForTopic(this.#caches.posts, topicId),
	      allowStaleOnError: !refresh,
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    });
	  }
	  /** 只读中央响应缓存;不进入 client、Scheduler 或传输层。 */
	  cachedTargetCandidate(candidate, rawPostNumber, options, rawTopicId = this.topicId) {
	    if (!this.#gateway.cachedTopicTarget) return Promise.resolve(null);
	    const postNumber = (0, import_identifiers.discoursePostNumber)(rawPostNumber), topicId = (0, import_identifiers.discourseTopicId)(rawTopicId);
	    return import_native_request_descriptors.DiscourseNativeRequests.targetCandidates({
	      basePath: this.#basePath,
	      topicId,
	      postNumber,
	      scope: options.scope,
	      ...options.slug === void 0 ? {} : { slug: options.slug },
	      refresh: options.refresh === !0
	    }).find((entry) => entry.endpoint === candidate.endpoint && entry.url === candidate.url) ? this.#gateway.cachedTopicTarget({
	      authScope: this.authScope,
	      topicId,
	      operation: `target:${options.scope}:${candidate.endpoint}`,
	      postNumber,
	      profile: options.background ? "background-prefetch" : "topic-visible",
	      cache: cacheForTopic(this.#caches.posts, topicId)
	    }) : Promise.reject(new Error("目标楼层缓存查询不属于 Discourse 原生目录"));
	  }
	  loadNestedReplies(rawParentPostNumber, options = {}) {
	    const parentPostNumber = (0, import_identifiers.discoursePostNumber)(rawParentPostNumber), parentPostId = options.parentPostId === void 0 ? void 0 : (0, import_identifiers.discoursePostId)(options.parentPostId), after = (0, import_identifiers.discourseReplyCursor)(options.after);
	    if (parentPostId === void 0)
	      throw new Error("直属回复 endpoint 需要 parentPostId");
	    const descriptor = import_native_request_descriptors.DiscourseNativeRequests.directReplies({
	      basePath: this.#basePath,
	      parentPostId,
	      after
	    }), requestLifetime = linkedRequestSignal(
	      this.#signal,
	      options.signal
	    );
	    return this.#gateway.loadNestedReplies({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      parentPostNumber,
	      parentPostId,
	      after,
	      profile: options.background ? "background-prefetch" : "nested-visible",
	      input: descriptor.path,
	      signal: requestLifetime.signal,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	      cacheMode: options.refresh ? "refresh" : "default",
	      cache: cacheWithPostIds(this.#caches.nested, [parentPostId]),
	      transport: (input) => this.#transport.request({
	        descriptor,
	        signal: input.signal,
	        attempt: input.attempt
	      })
	    }).finally(() => requestLifetime.dispose());
	  }
	  promoteNestedReplies(rawParentPostNumber, options = {}) {
	    const parentPostNumber = (0, import_identifiers.discoursePostNumber)(rawParentPostNumber), parentPostId = options.parentPostId === void 0 ? void 0 : (0, import_identifiers.discoursePostId)(options.parentPostId);
	    return parentPostId === void 0 ? !1 : this.#gateway.promoteNestedReplies?.({
	      authScope: this.authScope,
	      topicId: this.topicId,
	      parentPostNumber,
	      parentPostId,
	      after: (0, import_identifiers.discourseReplyCursor)(options.after),
	      profile: options.background ? "background-prefetch" : "nested-visible",
	      cacheMode: options.refresh ? "refresh" : "default"
	    }) ?? !1;
	  }
	}
}, "1aff93d89fa6c4282038878018b9f40d5b583b061e995ac8844a916308869742");

/* Source: lite/src/topic/topic-session.ts */
runtime.register("src/topic/topic-session.js", function(module, exports, require) {
	var topic_session_exports = {};
	__export(topic_session_exports, {
	  TopicSession: () => TopicSession,
	  discoursePostsFromPayload: () => discoursePostsFromPayload
	});
	module.exports = __toCommonJS(topic_session_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_native_request_descriptors = require("../discourse/native-request-descriptors.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js");
	function positiveInteger(value, name) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return numeric;
	}
	function nonNegativeInteger(value) {
	  const numeric = Number(value ?? 0);
	  return Number.isSafeInteger(numeric) && numeric >= 0 ? numeric : 0;
	}
	function directReplyPost(post, parentPostNumber) {
	  const rawParent = post.reply_to_post_number;
	  if (rawParent != null && rawParent !== "") {
	    let explicitParent;
	    try {
	      explicitParent = (0, import_identifiers.discoursePostReference)({
	        post_number: rawParent
	      }).postNumber;
	    } catch {
	      return null;
	    }
	    return explicitParent === parentPostNumber ? post : null;
	  }
	  return Object.freeze({
	    ...post,
	    reply_to_post_number: parentPostNumber
	  });
	}
	function scopedReplyPost(post, parentPostNumber) {
	  try {
	    (0, import_identifiers.discoursePostReference)(post);
	  } catch {
	    return null;
	  }
	  const direct = directReplyPost(post, parentPostNumber);
	  if (direct) return direct;
	  try {
	    return (0, import_identifiers.discoursePostReference)({
	      post_number: post.reply_to_post_number
	    }), post;
	  } catch {
	    return null;
	  }
	}
	function replyPageCursor(posts) {
	  let cursor = 0;
	  for (const post of posts)
	    try {
	      cursor = Math.max(cursor, (0, import_identifiers.discoursePostReference)(post).postNumber);
	    } catch {
	    }
	  return cursor;
	}
	function errorStatus(error) {
	  return Number(error?.status ?? 0);
	}
	function comparableRequestPath(value) {
	  const source = String(value ?? "").trim();
	  if (!source) return "";
	  try {
	    const url = new URL(source, "https://reader.invalid");
	    return `${url.pathname}${url.search}`;
	  } catch {
	    return source;
	  }
	}
	function isAuthFailure(error) {
	  return [401, 403].includes(errorStatus(error));
	}
	function isThrottleFailure(error) {
	  return errorStatus(error) === 429 || error?.cloudflareMitigated === !0;
	}
	function isAbortFailure(error) {
	  return error instanceof DOMException && error.name === "AbortError" || String(error?.name ?? "") === "AbortError";
	}
	function throwIfAborted(signal) {
	  if (signal?.aborted)
	    throw signal.reason ?? new DOMException("Aborted", "AbortError");
	}
	function awaitWithSignal(operation, signal) {
	  return signal ? signal.aborted ? Promise.reject(
	    signal.reason ?? new DOMException("Aborted", "AbortError")
	  ) : new Promise((resolve, reject) => {
	    let settled = !1;
	    const finish = (callback) => {
	      settled || (settled = !0, signal.removeEventListener("abort", onAbort), callback());
	    }, onAbort = () => finish(() => reject(
	      signal.reason ?? new DOMException("Aborted", "AbortError")
	    ));
	    signal.addEventListener("abort", onAbort, { once: !0 }), Promise.resolve(operation).then(
	      (value) => finish(() => resolve(value)),
	      (error) => finish(() => reject(error))
	    );
	  }) : Promise.resolve(operation);
	}
	function linkedAbortSignals(first, second) {
	  if (!first || first === second) {
	    const signal = second ?? first;
	    return Object.freeze(signal ? { signal, dispose() {
	    } } : { dispose() {
	    } });
	  }
	  if (!second) return Object.freeze({ signal: first, dispose() {
	  } });
	  const controller = new AbortController(), abortFromFirst = () => controller.abort(first.reason), abortFromSecond = () => controller.abort(second.reason);
	  return first.aborted ? abortFromFirst() : second.aborted ? abortFromSecond() : (first.addEventListener("abort", abortFromFirst, { once: !0 }), second.addEventListener("abort", abortFromSecond, { once: !0 })), Object.freeze({
	    signal: controller.signal,
	    dispose() {
	      first.removeEventListener("abort", abortFromFirst), second.removeEventListener("abort", abortFromSecond);
	    }
	  });
	}
	function shouldSplit(error) {
	  return [400, 404, 413, 414, 422].includes(errorStatus(error));
	}
	function freezeNumbers(values) {
	  return Object.freeze([...values]);
	}
	function discoursePostsFromPayload(payload) {
	  if (Array.isArray(payload)) return Object.freeze(payload.filter(Boolean));
	  if (!payload || typeof payload != "object") return Object.freeze([]);
	  const candidate = payload;
	  return Array.isArray(candidate.post_stream?.posts) ? Object.freeze(candidate.post_stream.posts.filter(Boolean)) : candidate.post_number === void 0 ? Object.freeze([]) : Object.freeze([payload]);
	}
	function postRecord(value) {
	  return value && typeof value == "object" ? value : null;
	}
	function moderationHiddenPlaceholder(value) {
	  const source = postRecord(value);
	  if (source?.hidden !== !0) return !1;
	  const cooked = String(source.cooked ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
	  return /社区举报.*临时隐藏/.test(cooked) || /flagged by the community.*temporarily hidden/i.test(cooked);
	}
	function cachedOriginalCooked(value) {
	  const source = postRecord(value);
	  if (!source || source.reader_local_archive_placeholder === !0 || moderationHiddenPlaceholder(value))
	    return null;
	  const cooked = typeof source.cooked == "string" ? source.cooked : "";
	  return cooked.trim() ? cooked : null;
	}
	class TopicSession {
	  topicId;
	  scope;
	  changes = new import_signal.Signal();
	  archiveChanges = new import_signal.Signal();
	  #requests;
	  #snapshots;
	  #replies;
	  #pageSize;
	  #refreshCachedInBackground;
	  #now;
	  #wait;
	  #signal;
	  #onError;
	  #onInitializeSource;
	  #postById = /* @__PURE__ */ new Map();
	  #postByNumber = /* @__PURE__ */ new Map();
	  #streamIndexByPostId = /* @__PURE__ */ new Map();
	  #cachedPostsSnapshot = null;
	  #pendingByPostId = /* @__PURE__ */ new Map();
	  #pendingDirectReplies = /* @__PURE__ */ new Map();
	  #unavailablePostNumbers = /* @__PURE__ */ new Set();
	  #streamPostIds = Object.freeze([]);
	  #postStreamRevision = 0;
	  #topic = null;
	  #cursor = 0;
	  #sequentialLoadStarted = !1;
	  #initializedFromCache = !1;
	  #initPromise = null;
	  #refreshPromise = null;
	  #postStreamPromise = null;
	  #postStreamExecution = null;
	  #authoritativeTopicExpectedPostCount = 0;
	  #createdPostNumbersSinceAuthoritativeTopic = /* @__PURE__ */ new Set();
	  #closed = !1;
	  #archiveStateKey = "";
	  constructor(options) {
	    if (this.topicId = (0, import_identifiers.discourseTopicId)(options.topicId), options.snapshots.topicId !== String(this.topicId))
	      throw new Error("TopicSession 与 TopicSnapshotRepository topicId 不一致");
	    if (options.replies.topicId !== String(this.topicId))
	      throw new Error("TopicSession 与 ReplyTreeRepository topicId 不一致");
	    this.#requests = options.requests, this.#snapshots = options.snapshots, this.#replies = options.replies, this.#pageSize = positiveInteger(options.pageSize, "pageSize"), this.#refreshCachedInBackground = options.refreshCachedInBackground !== !1, this.#now = options.now ?? Date.now, this.#wait = options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))), this.#signal = options.signal, this.#onError = options.onError ?? (() => {
	    }), this.#onInitializeSource = options.onInitializeSource ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.scope), this.scope.add(() => {
	      this.#closed = !0, this.#cachedPostsSnapshot = null, this.#pendingByPostId.clear();
	      for (const pending of this.#pendingDirectReplies.values())
	        pending.controller.signal.aborted || pending.controller.abort(new DOMException(
	          "Topic 已关闭",
	          "AbortError"
	        ));
	      this.#pendingDirectReplies.clear(), this.#postStreamPromise = null, this.#postStreamExecution = null;
	    });
	  }
	  get topic() {
	    return this.#topic;
	  }
	  get initializedFromCache() {
	    return this.#initializedFromCache;
	  }
	  get loadDone() {
	    return this.#cursor >= this.#streamPostIds.length;
	  }
	  streamPostIds() {
	    return this.#streamPostIds;
	  }
	  get postStreamRevision() {
	    return this.#postStreamRevision;
	  }
	  /**
	   * 两个已加载楼层之间实际存在的 canonical stream 项数。
	   *
	   * post_number 会因删除而留洞;只有 post_stream 的相对位置能够区分“已删除编号”
	   * 与“正文尚未水合”。索引随 stream 快照一次重建,根投影逐楼读取保持 O(1)。
	   */
	  postStreamGapCount(rawPreviousPostNumber, rawPostNumber) {
	    const postNumber = (0, import_identifiers.discoursePostNumber)(rawPostNumber), post = this.#postByNumber.get(postNumber);
	    if (!post) return;
	    const postId = (0, import_identifiers.discoursePostReference)(post).postId;
	    if (postId === null) return;
	    const postIndex = this.#streamIndexByPostId.get(postId);
	    if (postIndex === void 0) return;
	    let previousIndex = -1;
	    if (rawPreviousPostNumber > 0) {
	      const previousPost = this.#postByNumber.get(
	        (0, import_identifiers.discoursePostNumber)(rawPreviousPostNumber)
	      );
	      if (!previousPost) return;
	      const previousPostId = (0, import_identifiers.discoursePostReference)(previousPost).postId;
	      if (previousPostId === null) return;
	      const resolvedPreviousIndex = this.#streamIndexByPostId.get(previousPostId);
	      if (resolvedPreviousIndex === void 0) return;
	      previousIndex = resolvedPreviousIndex;
	    }
	    if (!(postIndex <= previousIndex))
	      return postIndex - previousIndex - 1;
	  }
	  postStreamCoverage() {
	    const expectedPostCount = this.#snapshots.snapshot().expectedPostCount, missingPostCount = this.#streamPostIds.reduce(
	      (total, postId) => total + (this.#postById.has(postId) ? 0 : 1),
	      0
	    );
	    return Object.freeze({
	      complete: expectedPostCount > 0 && expectedPostCount <= this.#streamPostIds.length && missingPostCount === 0,
	      expectedPostCount,
	      streamPostCount: this.#streamPostIds.length,
	      missingPostCount
	    });
	  }
	  unavailablePostNumbers() {
	    return Object.freeze(
	      [...this.#unavailablePostNumbers].sort((left, right) => left - right)
	    );
	  }
	  localArchiveState() {
	    return this.#snapshots.localArchiveState();
	  }
	  get pageSize() {
	    return this.#pageSize;
	  }
	  /**
	   * 原地切换后续 loader 批次大小;不移动 cursor、不清 pending,也不重建 Topic。
	   */
	  applyPageSize(pageSize) {
	    this.#assertActive(), this.#pageSize = positiveInteger(pageSize, "pageSize");
	  }
	  cachedPosts() {
	    return this.#cachedPostsSnapshot ??= Object.freeze([...this.#postByNumber.entries()].sort(([left], [right]) => left - right).map(([, post]) => post)), this.#cachedPostsSnapshot;
	  }
	  postById(rawPostId) {
	    return this.#postById.get((0, import_identifiers.discoursePostId)(rawPostId));
	  }
	  postByNumber(rawPostNumber) {
	    return this.#postByNumber.get((0, import_identifiers.discoursePostNumber)(rawPostNumber));
	  }
	  init(options = {}) {
	    if (this.#assertActive(), this.#topic) return Promise.resolve(this.#topic);
	    if (this.#initPromise) return this.#initPromise;
	    const promise = this.#initialize(options);
	    return this.#initPromise = promise, promise.finally(() => {
	      this.#initPromise === promise && (this.#initPromise = null);
	    }).catch(() => {
	    }), promise;
	  }
	  refresh(options = {}) {
	    if (this.#assertActive(), this.#isLocalArchiveTopic()) {
	      const cached = this.#topic ?? this.#snapshots.topic();
	      if (cached) return Promise.resolve(cached);
	    }
	    return this.#loadTopic(options, !0);
	  }
	  #loadTopic(options, refresh) {
	    if (this.#refreshPromise) return this.#refreshPromise;
	    const observedAt = this.#now(), promise = this.#requests.loadTopic({
	      ...options,
	      refresh
	    }).then(async (topic) => {
	      this.#assertActive();
	      const posts = await this.#prepareModerationHiddenPosts(
	        discoursePostsFromPayload(topic)
	      );
	      return this.#assertActive(), this.#commitTopic(topic, "topic-json", observedAt, posts), topic;
	    }).catch((error) => {
	      this.#assertActive();
	      const status = errorStatus(error), cached = this.#snapshots.topic();
	      if (![403, 404, 410].includes(status) || cached === null || this.#snapshots.posts().length === 0) throw error;
	      return this.#snapshots.markTopicUnavailable(status, observedAt), this.#restoreIndexes(), this.#initializedFromCache = !0, this.#syncLocalArchiveState(), cached;
	    });
	    return this.#refreshPromise = promise, promise.finally(() => {
	      this.#refreshPromise === promise && (this.#refreshPromise = null);
	    }).catch(() => {
	    }), promise;
	  }
	  async next(options = {}) {
	    this.#assertActive(), this.#sequentialLoadStarted = !0;
	    const position = this.#cursor;
	    if (position >= this.#streamPostIds.length)
	      return this.#batchResult([], !0, [], !1, !1);
	    const ids = this.#streamPostIds.slice(position, position + this.#pageSize), missingBefore = ids.filter((postId) => !this.#postById.has(postId)), localArchive = this.#isLocalArchiveTopic();
	    if (options.onSource?.(localArchive || !missingBefore.length ? "cache" : "network", Object.freeze({
	      cachedCount: ids.length - missingBefore.length,
	      missingCount: missingBefore.length,
	      totalCount: ids.length
	    })), localArchive) {
	      const posts2 = ids.map((postId) => this.#postById.get(postId)).filter((post) => post !== void 0);
	      return this.#cursor += ids.length, this.#batchResult(
	        posts2,
	        this.#cursor >= this.#streamPostIds.length,
	        missingBefore,
	        !1,
	        !1
	      );
	    }
	    let loadError;
	    if (missingBefore.length)
	      try {
	        await this.loadPostsByIds(ids, {
	          ...options.background === void 0 ? {} : { background: options.background },
	          ...options.priority === void 0 ? {} : { priority: options.priority },
	          ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork },
	          ...options.maxAttempts === void 0 ? {} : { maxAttempts: options.maxAttempts },
	          ...options.beforeCommit === void 0 ? {} : { beforeCommit: options.beforeCommit }
	        });
	      } catch (error) {
	        if (isThrottleFailure(error)) throw error;
	        loadError = error;
	      }
	    const missing = ids.filter((postId) => !this.#postById.has(postId));
	    if (missing.length) {
	      const fatal = loadError !== void 0 && isAuthFailure(loadError);
	      return this.#batchResult(
	        [],
	        !1,
	        missing,
	        !fatal,
	        fatal,
	        loadError
	      );
	    }
	    const posts = ids.map((postId) => this.#postById.get(postId)).filter((post) => post !== void 0);
	    return this.#cursor += ids.length, this.#batchResult(
	      posts,
	      this.#cursor >= this.#streamPostIds.length,
	      [],
	      !1,
	      !1
	    );
	  }
	  async loadPostsByIds(rawPostIds, options = {}) {
	    this.#assertActive();
	    const postIds = (0, import_identifiers.discoursePostIdStream)(rawPostIds);
	    if (this.#isLocalArchiveTopic()) {
	      const missingPostIds2 = postIds.filter((postId) => !this.#postById.has(postId));
	      return Object.freeze({
	        posts: Object.freeze(
	          postIds.map((postId) => this.#postById.get(postId)).filter((post) => post !== void 0)
	        ),
	        missingPostIds: freezeNumbers(missingPostIds2)
	      });
	    }
	    const maxAttempts = positiveInteger(options.maxAttempts ?? 2, "maxAttempts");
	    let lastError;
	    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
	      const pending = attempt === 0 && options.refresh ? postIds : postIds.filter((postId) => !this.#postById.has(postId));
	      if (!pending.length) break;
	      try {
	        await this.#ensurePostIds(pending, options), lastError = void 0;
	      } catch (error) {
	        if (lastError = error, isAuthFailure(error) || isThrottleFailure(error)) throw error;
	      }
	      postIds.some((postId) => !this.#postById.has(postId)) && attempt + 1 < maxAttempts && await this.#wait(240);
	    }
	    const missingPostIds = postIds.filter((postId) => !this.#postById.has(postId));
	    if (missingPostIds.length && lastError !== void 0) throw lastError;
	    return Object.freeze({
	      posts: Object.freeze(
	        postIds.map((postId) => this.#postById.get(postId)).filter((post) => post !== void 0)
	      ),
	      missingPostIds: freezeNumbers(missingPostIds)
	    });
	  }
	  /**
	   * 用 Discourse post_ids[] 批量端点预热当前顺序流之后最近仍缺正文的完整批次。
	   *
	   * 顺序流尚未启动时保留“当前批次之后”的显式预热语义;next() 已启动后则从当前
	   * cursor 向前扫描,跳过完整缓存和整批在途请求。这样缓存命中同步推进 cursor 时不会
	   * 留下最近批次冷缺口,网络批次又仍可通过 pendingByPostId 保持单飞。
	   */
	  async prefetchAhead(rawBatchCount, options = {}) {
	    this.#assertActive();
	    const batchCount = Math.min(
	      2,
	      positiveInteger(rawBatchCount, "batchCount")
	    ), firstOffset = this.#sequentialLoadStarted ? this.#cursor : this.#cursor + this.#pageSize, batches = [];
	    for (let offset = firstOffset; offset < this.#streamPostIds.length && batches.length < batchCount; offset += this.#pageSize) {
	      const batch = this.#streamPostIds.slice(offset, offset + this.#pageSize), missing = batch.filter((postId) => !this.#postById.has(postId));
	      !missing.length || missing.every((postId) => this.#pendingByPostId.has(postId)) || batches.push([...batch]);
	    }
	    return Object.freeze(await Promise.all(batches.map((batch) => this.loadPostsByIds(batch, options))));
	  }
	  /**
	   * 补齐当前 Topic 的 canonical post.id stream。
	   *
	   * 需要全帖投影的功能(例如图片关联评论、全帖媒体索引)共用本入口;调用方不得再复制
	   * stream 分批、single-flight、缺口判断或第二份 post Map。失败批次不会阻断其余批次,
	   * 最终以 missingPostIds/complete 明确报告覆盖率。
	   */
	  ensurePostStream(options = {}) {
	    if (this.#assertActive(), this.#isLocalArchiveTopic()) {
	      const missingPostIds = this.#streamPostIds.filter(
	        (postId) => !this.#postById.has(postId)
	      );
	      return options.onProgress?.(Object.freeze({
	        loadedCount: this.#streamPostIds.length - missingPostIds.length,
	        totalCount: this.#streamPostIds.length,
	        missingCount: missingPostIds.length
	      })), Promise.resolve(Object.freeze({
	        posts: this.cachedPosts(),
	        missingPostIds: freezeNumbers(missingPostIds),
	        complete: this.postStreamCoverage().complete,
	        failedBatchCount: 0
	      }));
	    }
	    if (this.#postStreamPromise)
	      return this.#upgradePostStreamExecution(options), this.#postStreamPromise;
	    const execution = {
	      background: options.background === !0,
	      priority: options.priority,
	      maxAttempts: positiveInteger(options.maxAttempts ?? 2, "maxAttempts"),
	      refresh: options.refresh === !0
	    };
	    this.#postStreamExecution = execution;
	    const request = this.#loadPostStream(options, execution).finally(() => {
	      this.#postStreamPromise === request && (this.#postStreamPromise = null, this.#postStreamExecution === execution && (this.#postStreamExecution = null));
	    });
	    return this.#postStreamPromise = request, request;
	  }
	  #upgradePostStreamExecution(options) {
	    const execution = this.#postStreamExecution;
	    if (!execution) return;
	    const wasBackground = execution.background, previousPriority = execution.priority;
	    options.background !== !0 && (execution.background = !1), options.priority === "nested" ? execution.priority = "nested" : execution.priority === void 0 && options.priority === "visible" && (execution.priority = "visible"), options.maxAttempts !== void 0 && (execution.maxAttempts = Math.max(
	      execution.maxAttempts,
	      positiveInteger(options.maxAttempts, "maxAttempts")
	    )), !(wasBackground === execution.background && previousPriority === execution.priority) && this.#promotePendingPostBatches({
	      background: execution.background,
	      ...execution.priority === void 0 ? {} : { priority: execution.priority },
	      ...execution.refresh ? { refresh: !0 } : {}
	    });
	  }
	  async #loadPostStream(options, execution) {
	    let loadedCount = 0;
	    const report = () => {
	      options.onProgress?.(Object.freeze({
	        loadedCount,
	        totalCount: this.#streamPostIds.length,
	        missingCount: this.#streamPostIds.length - loadedCount
	      }));
	    };
	    let failedBatchCount = 0;
	    if (this.#snapshots.snapshot().expectedPostCount > this.#streamPostIds.length)
	      try {
	        await this.refresh({
	          background: execution.background,
	          ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork }
	        });
	      } catch (error) {
	        if (this.#closed || this.scope.destroyed || isThrottleFailure(error)) throw error;
	        failedBatchCount += 1, this.#onError(error);
	      }
	    loadedCount = this.#streamPostIds.reduce(
	      (total, postId) => total + Number(this.#postById.has(postId)),
	      0
	    ), report();
	    for (let offset = 0; offset < this.#streamPostIds.length; ) {
	      const batch = this.#streamPostIds.slice(
	        offset,
	        offset + this.#pageSize
	      );
	      if (!batch.length) break;
	      offset += batch.length;
	      const loadedBefore = batch.reduce(
	        (total, postId) => total + Number(this.#postById.has(postId)),
	        0
	      );
	      if (loadedBefore === batch.length) continue;
	      try {
	        await options.beforeBatch?.(), await this.loadPostsByIds(batch, {
	          ...options,
	          background: execution.background,
	          maxAttempts: execution.maxAttempts,
	          ...execution.priority === void 0 ? {} : { priority: execution.priority }
	        });
	      } catch (error) {
	        if (this.#closed || this.scope.destroyed || isThrottleFailure(error)) throw error;
	        failedBatchCount += 1, this.#onError(error);
	      }
	      const loadedAfter = batch.reduce(
	        (total, postId) => total + Number(this.#postById.has(postId)),
	        0
	      );
	      loadedAfter !== loadedBefore && (loadedCount += loadedAfter - loadedBefore, report());
	    }
	    const missingPostIds = this.#streamPostIds.filter(
	      (postId) => !this.#postById.has(postId)
	    ), coverage = this.postStreamCoverage();
	    return Object.freeze({
	      posts: this.cachedPosts(),
	      missingPostIds: freezeNumbers(missingPostIds),
	      complete: coverage.complete,
	      failedBatchCount
	    });
	  }
	  async loadPostById(rawPostId, options = {}) {
	    this.#assertActive();
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId);
	    if (this.#isLocalArchiveTopic()) return this.#postById.get(postId) ?? null;
	    const observedAt = this.#now(), payload = await this.#requests.loadPostById(postId, options), matchingPost = discoursePostsFromPayload(payload).find((post) => {
	      try {
	        return (0, import_identifiers.discoursePostReference)(post).postId === postId;
	      } catch {
	        return !1;
	      }
	    });
	    return matchingPost ? (options.created === !0 || !this.#streamPostIds.includes(postId) ? this.ingestCreatedPost(matchingPost, "target-refresh", observedAt) : this.#commit({ posts: [matchingPost] }, "target-refresh", observedAt), this.#postById.get(postId) ?? null) : null;
	  }
	  /**
	   * Discourse 直属回复 endpoint 的唯一 canonical 入口。
	   *
	   * endpoint 已由 parent post.id 限定;部分站点会省略 reply_to_post_number,
	   * 因而在提交快照/树之前必须补回父级提示。显式指向其他父级的帖子按自身 canonical
	   * 关系提交,但不混入当前直属集合;调用者可通过 scopedPosts 保留 endpoint 讨论语义。
	   */
	  loadDirectReplies(rawParentPostNumber, options = {}) {
	    this.#assertActive(), throwIfAborted(options.signal);
	    const parentPostNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawParentPostNumber
	    }).postNumber, refresh = options.refresh === !0, key = `${parentPostNumber}:${refresh ? "refresh" : "default"}`, pending = this.#pendingDirectReplies.get(key);
	    if (pending)
	      return options.background !== !0 && (pending.profile.background = !1, pending.profile.activeRequest && this.#requests.promoteNestedReplies?.(
	        parentPostNumber,
	        {
	          ...pending.profile.activeRequest,
	          background: !1
	        }
	      )), this.#joinDirectReplies(pending, options.signal);
	    const controller = new AbortController(), requestLifetime = linkedAbortSignals(
	      this.#signal,
	      controller.signal
	    ), profile = {
	      background: options.background === !0,
	      activeRequest: null
	    };
	    let created;
	    return created = {
	      task: this.#loadDirectReplies(parentPostNumber, {
	        ...options,
	        signal: requestLifetime.signal ?? controller.signal
	      }, profile).finally(() => {
	        created.settled = !0, requestLifetime.dispose(), this.#pendingDirectReplies.get(key) === created && this.#pendingDirectReplies.delete(key);
	      }),
	      controller,
	      consumers: /* @__PURE__ */ new Set(),
	      profile,
	      unabortableConsumer: !1,
	      settled: !1
	    }, this.#pendingDirectReplies.set(key, created), this.#joinDirectReplies(created, options.signal);
	  }
	  #joinDirectReplies(pending, signal) {
	    if (throwIfAborted(signal), !signal)
	      return pending.unabortableConsumer = !0, pending.task;
	    const consumer = Symbol("direct-replies-consumer");
	    return pending.consumers.add(consumer), awaitWithSignal(pending.task, signal).finally(() => {
	      pending.consumers.delete(consumer), !(pending.settled || pending.unabortableConsumer || pending.consumers.size > 0 || pending.controller.signal.aborted) && pending.controller.abort(
	        signal.reason ?? new DOMException(
	          "直属回复已无消费者",
	          "AbortError"
	        )
	      );
	    });
	  }
	  /**
	   * 从一个或多个已加载根楼层递归补齐子孙分支。
	   *
	   * 完整讨论、阅读队列等上层只提交根集合;遍历、直属分页、partial 统计与错误保留均由
	   * TopicSession 统一完成,避免每个 surface 维护自己的树扫描和请求循环。
	   */
	  async loadReplyBranches(rawRootPostNumbers, options = {}) {
	    this.#assertActive();
	    const rootPostNumbers = Object.freeze([...new Set(
	      rawRootPostNumbers.map((postNumber) => (0, import_identifiers.discoursePostReference)({
	        post_number: postNumber
	      }).postNumber)
	    )]), pending = [...rootPostNumbers], queued = new Set(pending), enqueue = (postNumber) => {
	      queued.has(postNumber) || (queued.add(postNumber), pending.push(postNumber));
	    }, seen = /* @__PURE__ */ new Set(), postNumbers = [], parentPostNumbers = [], contextualReplyRelations = [], contextualRelationKeys = /* @__PURE__ */ new Set(), errors = [];
	    let expectedReplyCount = 0, loadedReplyCount = 0, complete = !0;
	    const report = (processedCount) => {
	      options.onProgress?.(Object.freeze({
	        processedCount,
	        totalCount: pending.length,
	        loadedReplyCount,
	        expectedReplyCount
	      }));
	    };
	    report(0);
	    for (let index = 0; index < pending.length; index += 1) {
	      this.#assertActive();
	      const postNumber = pending[index];
	      if (seen.has(postNumber)) {
	        report(index + 1);
	        continue;
	      }
	      seen.add(postNumber);
	      const post = this.#postByNumber.get(postNumber);
	      if (!post) {
	        complete = !1, report(index + 1);
	        continue;
	      }
	      postNumbers.push(postNumber);
	      const expectedCount = Math.max(
	        nonNegativeInteger(post.reply_count),
	        this.#replies.topology.childrenOf(postNumber).length
	      );
	      if (expectedCount > 0) {
	        parentPostNumbers.push(postNumber);
	        let directPosts = this.#knownDirectReplies(postNumber), scopedPosts = directPosts;
	        if (directPosts.length < expectedCount)
	          try {
	            const result = await this.loadDirectReplies(postNumber, {
	              ...options,
	              expectedCount
	            });
	            directPosts = result.posts, scopedPosts = result.scopedPosts, complete = complete && result.complete;
	          } catch (error) {
	            let finalError = error;
	            if (isAbortFailure(error) && !options.signal?.aborted && !this.scope.destroyed)
	              try {
	                const recovered = await this.loadDirectReplies(postNumber, {
	                  ...options,
	                  expectedCount,
	                  background: !1
	                });
	                directPosts = recovered.posts, scopedPosts = recovered.scopedPosts, complete = complete && recovered.complete, finalError = null;
	              } catch (recoveryError) {
	                finalError = recoveryError;
	              }
	            if (finalError !== null) {
	              if (this.scope.destroyed || isAbortFailure(finalError) || isThrottleFailure(finalError)) throw finalError;
	              errors.push(finalError), complete = !1;
	            }
	          }
	        const scopedPostNumbers = /* @__PURE__ */ new Set();
	        for (const scopedPost of scopedPosts) {
	          const childPostNumber = (0, import_identifiers.discoursePostReference)(scopedPost).postNumber;
	          if (childPostNumber === postNumber || (scopedPostNumbers.add(childPostNumber), enqueue(childPostNumber), this.#canonicalBranchContains(postNumber, childPostNumber))) continue;
	          const key = `${postNumber}:${childPostNumber}`;
	          contextualRelationKeys.has(key) || (contextualRelationKeys.add(key), contextualReplyRelations.push(Object.freeze({
	            parentPostNumber: postNumber,
	            postNumber: childPostNumber
	          })));
	        }
	        for (const directPost of directPosts)
	          scopedPostNumbers.add((0, import_identifiers.discoursePostReference)(directPost).postNumber);
	        expectedReplyCount += expectedCount, loadedReplyCount += scopedPostNumbers.size, complete = complete && scopedPostNumbers.size >= expectedCount;
	      }
	      for (const child of this.#replies.topology.childrenOf(postNumber))
	        enqueue((0, import_identifiers.discoursePostReference)({
	          post_number: child
	        }).postNumber);
	      report(index + 1);
	    }
	    return Object.freeze({
	      rootPostNumbers,
	      postNumbers: Object.freeze(postNumbers),
	      parentPostNumbers: Object.freeze(parentPostNumbers),
	      expectedReplyCount,
	      loadedReplyCount,
	      complete,
	      contextualReplyRelations: Object.freeze(contextualReplyRelations),
	      errors: Object.freeze(errors)
	    });
	  }
	  /**
	   * 用户 reply/create 与 MessageBus created 共用的原子 ingress。
	   *
	   * 新 post 在同一次 commit 中进入 post.id stream、正文索引、快照和回复拓扑;已知 post
	   * 只更新正文/关系,不重复增加 stream 或 expected count。
	   */
	  ingestCreatedPost(post, source, observedAt = this.#now()) {
	    this.#assertActive();
	    const reference = (0, import_identifiers.discoursePostReference)(post);
	    if (reference.postId === null) throw new Error("created 楼层缺少 post.id");
	    const knownPostAtNumber = this.#postByNumber.get(reference.postNumber), knownPostIdAtNumber = knownPostAtNumber === void 0 ? null : (0, import_identifiers.discoursePostReference)(knownPostAtNumber).postId, isNewCreatedFloor = knownPostAtNumber === void 0, nextStream = this.#streamWithCreatedPost(
	      reference.postId,
	      reference.postNumber
	    );
	    isNewCreatedFloor && this.#createdPostNumbersSinceAuthoritativeTopic.add(
	      reference.postNumber
	    );
	    const expectedPostCount = nextStream === void 0 ? void 0 : this.#authoritativeTopicExpectedPostCount + this.#createdPostNumbersSinceAuthoritativeTopic.size, repairedChildren = [...this.#postByNumber.values()].filter((candidate) => {
	      try {
	        return (0, import_identifiers.discoursePostReference)(candidate).replyToPostNumber === reference.postNumber;
	      } catch {
	        return !1;
	      }
	    });
	    return this.#commit({
	      posts: [post, ...repairedChildren],
	      ...nextStream === void 0 ? {} : {
	        streamPostIds: knownPostIdAtNumber === null ? nextStream : nextStream.filter((postId) => postId !== knownPostIdAtNumber)
	      },
	      ...expectedPostCount === void 0 ? {} : { expectedPostCount }
	    }, source, observedAt);
	  }
	  removePostById(rawPostId, source = "action-response", observedAt = this.#now()) {
	    this.#assertActive();
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId), post = this.#postById.get(postId);
	    if (!post) throw new Error(`canonical post.id ${postId} 尚未加载`);
	    const reference = (0, import_identifiers.discoursePostReference)(post), snapshotResult = this.#snapshots.removePost(
	      reference.postNumber,
	      postId,
	      source,
	      observedAt
	    ), treeEvent = this.#replies.remove(
	      reference.postNumber,
	      source,
	      { observedAt }
	    );
	    this.#restoreIndexes();
	    const changedPostNumbers = Object.freeze(
	      [.../* @__PURE__ */ new Set([
	        reference.postNumber,
	        ...treeEvent?.change.changedPostNumbers ?? []
	      ])].sort((left, right) => left - right)
	    ), result = Object.freeze({
	      source,
	      observedAt,
	      acceptedPosts: 0,
	      ignoredPosts: snapshotResult.removed ? 0 : 1,
	      changedPostNumbers,
	      removedPostNumbers: snapshotResult.removed ? Object.freeze([reference.postNumber]) : Object.freeze([]),
	      topicChanged: !1,
	      streamChanged: snapshotResult.streamChanged
	    });
	    for (const error of this.changes.emit(result)) this.#onError(error);
	    return result;
	  }
	  preserveDeletedPostById(rawPostId, observedAt = this.#now()) {
	    this.#assertActive();
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId), post = this.#postById.get(postId);
	    if (!post) throw new Error(`canonical post.id ${postId} 尚未加载`);
	    const postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber, topicArchived = postNumber === 1 && this.#snapshots.markTopicUnavailable(404, observedAt), postArchived = postNumber !== 1 && this.#snapshots.markPostUnavailable(postNumber, 404, observedAt);
	    return postArchived && this.#unavailablePostNumbers.add(postNumber), (topicArchived || postArchived) && this.#syncLocalArchiveState(), Object.freeze({ postNumber, topicArchived });
	  }
	  /**
	   * 已确认失效的楼层只在 canonical 快照已有正文时转为只读存档。
	   *
	   * 岁月史书等入口传入的是已观测到的服务器状态;本方法不发请求、
	   * 不伪造正文,也不在缓存缺失时创建占位楼层。
	   */
	  preserveUnavailablePost(rawPostNumber, status, confirmedAt = this.#now()) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber;
	    if (!this.#postByNumber.has(postNumber)) return !1;
	    const archived = postNumber === 1 ? this.#snapshots.markTopicUnavailable(status, confirmedAt) : this.#snapshots.markPostUnavailable(
	      postNumber,
	      status,
	      confirmedAt
	    );
	    return postNumber !== 1 && this.#unavailablePostNumbers.add(postNumber), archived && this.#syncLocalArchiveState(), !0;
	  }
	  /**
	   * 当 Topic 快照尚无正文或只剩举报占位文案时,从中央响应缓存找回曾经成功
	   * 返回的目标楼层。只提交精确目标,不联网、不把整份旧 Topic payload 覆盖
	   * 当前 canonical 状态。
	   */
	  async restoreUnavailablePostFromCache(rawPostNumber, status, confirmedAt = this.#now(), preferredRequestPath) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber, current = this.#postByNumber.get(postNumber);
	    if (current && !moderationHiddenPlaceholder(current) && this.preserveUnavailablePost(postNumber, status, confirmedAt))
	      return !0;
	    const target = await this.#cachedOriginalPost(
	      postNumber,
	      preferredRequestPath
	    ), originalCooked = cachedOriginalCooked(target);
	    if (target && originalCooked) {
	      const currentPost = this.#postByNumber.get(postNumber), restored = currentPost && moderationHiddenPlaceholder(currentPost) ? Object.freeze({
	        ...currentPost,
	        cooked: originalCooked
	      }) : target;
	      if (this.ingestPosts(
	        [restored],
	        currentPost ? "target-refresh" : "loader-batch",
	        confirmedAt
	      ), this.preserveUnavailablePost(postNumber, status, confirmedAt))
	        return !0;
	    }
	    return this.preserveUnavailablePost(postNumber, status, confirmedAt);
	  }
	  async #cachedOriginalPost(postNumber, preferredRequestPath) {
	    const readCached = this.#requests.cachedTargetCandidate;
	    if (!readCached) return null;
	    const slug = String(this.#topic?.slug ?? "topic"), plans = ["single", "around"].flatMap((scope) => {
	      const options = Object.freeze({
	        scope,
	        slug,
	        refresh: !0
	      });
	      return this.#requests.targetCandidates(postNumber, options).map(
	        (candidate) => Object.freeze({ candidate, options })
	      );
	    }), preferred = comparableRequestPath(preferredRequestPath), matching = preferred ? plans.filter(({ candidate }) => comparableRequestPath(candidate.url) === preferred) : [];
	    for (const { candidate, options } of matching.length ? matching : plans) {
	      let payload;
	      try {
	        payload = await readCached.call(
	          this.#requests,
	          candidate,
	          postNumber,
	          options
	        );
	      } catch (error) {
	        this.#onError(error);
	        continue;
	      }
	      if (payload === null) continue;
	      const target = discoursePostsFromPayload(payload).find((post) => {
	        try {
	          return (0, import_identifiers.discoursePostReference)(post).postNumber === postNumber;
	        } catch {
	          return !1;
	        }
	      });
	      if (cachedOriginalCooked(target)) return target ?? null;
	    }
	    return null;
	  }
	  async #prepareModerationHiddenPosts(posts) {
	    let changed = !1;
	    const prepared = [];
	    for (const post of posts) {
	      if (!moderationHiddenPlaceholder(post)) {
	        prepared.push(post);
	        continue;
	      }
	      let postNumber;
	      try {
	        postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber;
	      } catch (error) {
	        this.#onError(error), prepared.push(post);
	        continue;
	      }
	      const currentCooked = cachedOriginalCooked(
	        this.#postByNumber.get(postNumber)
	      ), cached = currentCooked ? null : await this.#cachedOriginalPost(postNumber), originalCooked = currentCooked ?? cachedOriginalCooked(cached);
	      if (!originalCooked) {
	        prepared.push(post);
	        continue;
	      }
	      changed = !0, prepared.push(Object.freeze({
	        ...post,
	        cooked: originalCooked
	      }));
	    }
	    return changed ? Object.freeze(prepared) : posts;
	  }
	  async loadTarget(rawPostNumber, options = {}) {
	    this.#assertActive();
	    const postNumber = (0, import_identifiers.discoursePostReference)({ post_number: rawPostNumber }).postNumber, scope = options.scope ?? "single", shouldAdvance = options.advanceCursor ?? scope === "around", cachedBeforeRequest = this.#postByNumber.get(postNumber);
	    if (this.#isLocalArchiveTopic())
	      return cachedBeforeRequest && shouldAdvance && this.#advanceCursorPast([cachedBeforeRequest], postNumber), Object.freeze(cachedBeforeRequest ? [cachedBeforeRequest] : []);
	    if (!options.forceRefresh && this.#unavailablePostNumbers.has(postNumber))
	      return Object.freeze(cachedBeforeRequest ? [cachedBeforeRequest] : []);
	    const cached = scope === "single" && !options.forceRefresh ? this.#postByNumber.get(postNumber) : void 0;
	    if (cached)
	      return shouldAdvance && this.#advanceCursorPast([cached], postNumber), Object.freeze([cached]);
	    const targetOptions = {
	      scope,
	      slug: String(this.#topic?.slug ?? "topic"),
	      refresh: options.forceRefresh === !0,
	      ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork }
	    };
	    let fallback = Object.freeze([]), definitiveStatus = null, definitiveRequestPath;
	    for (const candidate of this.#requests.targetCandidates(postNumber, targetOptions)) {
	      const observedAt = this.#now();
	      try {
	        const payload = await this.#requests.loadTargetCandidate(
	          candidate,
	          postNumber,
	          targetOptions
	        ), posts = discoursePostsFromPayload(payload);
	        this.ingestPosts(posts, "target-refresh", observedAt);
	        const committedPosts = posts.map((post) => {
	          try {
	            return this.#postByNumber.get(
	              (0, import_identifiers.discoursePostReference)(post).postNumber
	            );
	          } catch {
	            return;
	          }
	        }).filter((post) => post !== void 0), found = posts.some((post) => {
	          try {
	            return (0, import_identifiers.discoursePostReference)(post).postNumber === postNumber;
	          } catch {
	            return !1;
	          }
	        }) ? this.#postByNumber.get(postNumber) : void 0;
	        if (found) {
	          const result = scope === "around" ? committedPosts : [found];
	          return shouldAdvance && this.#advanceCursorPast(result, postNumber), Object.freeze([...result]);
	        }
	        scope === "around" && committedPosts.length && !fallback.length && (fallback = committedPosts);
	      } catch (error) {
	        const status = errorStatus(error);
	        if ((0, import_native_request_descriptors.discourseNativeTargetFailureIsDefinitive)({
	          endpoint: candidate.endpoint,
	          scope,
	          status
	        })) {
	          definitiveStatus = status, definitiveRequestPath = candidate.url;
	          break;
	        }
	        if (scope === "single" && candidate.endpoint === "post-by-number" && status === 403 && cachedBeforeRequest !== void 0) {
	          definitiveStatus = 403, definitiveRequestPath = candidate.url;
	          continue;
	        }
	        if (isAuthFailure(error) || isThrottleFailure(error)) throw error;
	      }
	    }
	    if (scope === "around" && this.#streamPostIds.length) {
	      const posts = await this.loadAroundPost(postNumber, {
	        maxAttempts: 1,
	        refresh: options.forceRefresh === !0,
	        ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork }
	      });
	      if (posts.length)
	        return shouldAdvance && this.#advanceCursorPast(posts, postNumber), posts;
	    }
	    if (scope === "single" && definitiveStatus !== null && (this.#unavailablePostNumbers.add(postNumber), cachedBeforeRequest)) {
	      const confirmedAt = this.#now();
	      await this.restoreUnavailablePostFromCache(
	        postNumber,
	        definitiveStatus,
	        confirmedAt,
	        definitiveRequestPath
	      );
	      const archivedPost = this.#postByNumber.get(postNumber) ?? cachedBeforeRequest;
	      return shouldAdvance && this.#advanceCursorPast([archivedPost], postNumber), Object.freeze([archivedPost]);
	    }
	    return fallback.length && shouldAdvance && this.#advanceCursorPast(fallback, postNumber), Object.freeze([...fallback]);
	  }
	  loadBeforePost(postNumber, options = {}) {
	    return this.#loadRelative(postNumber, "before", {
	      ...options,
	      maxAttempts: options.maxAttempts ?? 1
	    });
	  }
	  loadAfterPost(postNumber, options = {}) {
	    return this.#loadRelative(postNumber, "after", {
	      ...options,
	      maxAttempts: options.maxAttempts ?? 1
	    });
	  }
	  /**
	   * 只使用 Topic 已知的 canonical post_stream,在目标楼层附近读取一批正文。
	   *
	   * 虚拟 gap 的楼层号来自估算,不是可直接访问的 Discourse route。这里不得走
	   * targetCandidates,否则删除楼层或稀疏编号会把一次补窗放大成 topic/by-number
	   * 候选循环;批次仍完整复用 loadPostsByIds 的缓存、single-flight 与 429 终态。
	   */
	  async loadAroundPost(rawPostNumber, options = {}) {
	    if (this.#assertActive(), !this.#streamPostIds.length) return Object.freeze([]);
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber, center = this.#streamIndexForPostNumber(postNumber), start = Math.max(
	      0,
	      Math.min(
	        Math.max(0, this.#streamPostIds.length - this.#pageSize),
	        center - Math.floor(this.#pageSize / 2)
	      )
	    );
	    return (await this.loadPostsByIds(
	      this.#streamPostIds.slice(start, start + this.#pageSize),
	      {
	        ...options,
	        maxAttempts: options.maxAttempts ?? 1
	      }
	    )).posts;
	  }
	  /**
	   * 宿主 Topic 列表入口的标准正文预热窗口。
	   *
	   * #1 只取向下一个 pageSize;中间楼层取前后各一个 pageSize,目标楼层归入
	   * 向下窗口。所有批次仍走 loadPostsByIds 的缓存、single-flight 与中央后台调度;
	   * 完整缓存命中时不创建网络请求。
	   */
	  async restorePreheatEntry(rawPostNumber) {
	    if (this.#assertActive(), !this.#topic && !this.#streamPostIds.length && !this.#snapshots.posts().length) {
	      const restored = await this.#snapshots.restore();
	      if (this.#assertActive(), !restored) return null;
	      this.#restoreIndexes();
	    }
	    if (!this.#streamPostIds.length) return null;
	    const { ids, totalCount } = this.#entryPreheatWindow(rawPostNumber), warmedCount = ids.reduce(
	      (count, postId) => count + Number(this.#postById.has(postId)),
	      0
	    );
	    return Object.freeze({
	      warmedCount,
	      requestedCount: ids.length,
	      totalCount,
	      cacheHit: warmedCount >= ids.length,
	      complete: warmedCount >= ids.length
	    });
	  }
	  async preheatEntry(rawPostNumber, options = {}) {
	    this.#assertActive();
	    const { onProgress, minimumTotalCount: rawMinimumTotalCount, ...loadOptions } = options;
	    nonNegativeInteger(rawMinimumTotalCount) > Math.max(
	      this.#streamPostIds.length,
	      this.#snapshots.snapshot().expectedPostCount
	    ) && await this.refresh({
	      ...loadOptions.background === void 0 ? {} : { background: loadOptions.background },
	      ...loadOptions.prefetchTier === void 0 ? {} : { prefetchTier: loadOptions.prefetchTier },
	      ...loadOptions.beforeNetwork === void 0 ? {} : { beforeNetwork: loadOptions.beforeNetwork }
	    });
	    const { ids, totalCount } = this.#entryPreheatWindow(rawPostNumber);
	    if (!this.#streamPostIds.length) {
	      const empty = Object.freeze({
	        posts: Object.freeze([]),
	        missingPostIds: Object.freeze([]),
	        warmedCount: 0,
	        requestedCount: 0,
	        totalCount,
	        cacheHit: !0,
	        complete: !0
	      });
	      return onProgress?.(empty), empty;
	    }
	    const cacheHit = ids.every((postId) => this.#postById.has(postId)), progress = () => {
	      const warmedCount = ids.reduce(
	        (count, postId) => count + Number(this.#postById.has(postId)),
	        0
	      );
	      return Object.freeze({
	        warmedCount,
	        requestedCount: ids.length,
	        totalCount,
	        cacheHit,
	        complete: warmedCount >= ids.length
	      });
	    };
	    for (let offset = 0; offset < ids.length; offset += this.#pageSize) {
	      const batch = ids.slice(offset, offset + this.#pageSize);
	      batch.some((postId) => !this.#postById.has(postId)) && await this.loadPostsByIds(batch, loadOptions), onProgress?.(progress());
	    }
	    const finalProgress = progress();
	    return ids.length || onProgress?.(finalProgress), Object.freeze({
	      posts: Object.freeze(ids.map((postId) => this.#postById.get(postId)).filter((post) => post !== void 0)),
	      missingPostIds: freezeNumbers(ids.filter(
	        (postId) => !this.#postById.has(postId)
	      )),
	      ...finalProgress
	    });
	  }
	  #entryPreheatWindow(rawPostNumber) {
	    const postNumber = (0, import_identifiers.discoursePostReference)({
	      post_number: rawPostNumber
	    }).postNumber, totalCount = Math.max(
	      this.#streamPostIds.length,
	      this.#snapshots.snapshot().expectedPostCount
	    );
	    if (!this.#streamPostIds.length)
	      return Object.freeze({ ids: Object.freeze([]), totalCount });
	    const targetIndex = this.#streamIndexForPostNumber(postNumber), start = targetIndex <= 0 ? 0 : Math.max(0, targetIndex - this.#pageSize), end = Math.min(
	      this.#streamPostIds.length,
	      targetIndex <= 0 ? this.#pageSize : targetIndex + this.#pageSize
	    );
	    return Object.freeze({
	      ids: this.#streamPostIds.slice(start, end),
	      totalCount
	    });
	  }
	  loadLastPost(options = {}) {
	    return this.#loadRelative(1, "last", options);
	  }
	  ingestPosts(posts, source, observedAt = this.#now()) {
	    return this.#assertActive(), this.#commit({ posts }, source, observedAt);
	  }
	  ingestTopic(topic, source, observedAt = this.#now()) {
	    return this.#assertActive(), this.#commitTopic(topic, source, observedAt);
	  }
	  async flush() {
	    await this.#replies.flush(), await this.#snapshots.flush();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #loadDirectReplies(parentPostNumber, options, profile) {
	    const parentPost = this.#postByNumber.get(parentPostNumber);
	    if (!parentPost) throw new Error(`父楼层 #${parentPostNumber} 尚未加载`);
	    const parentPostId = (0, import_identifiers.discoursePostReference)(parentPost).postId;
	    if (parentPostId === null)
	      throw new Error(`父楼层 #${parentPostNumber} 缺少 post.id`);
	    const configuredExpected = nonNegativeInteger(options.expectedCount), declaredExpected = nonNegativeInteger(parentPost.reply_count);
	    let posts = this.#knownDirectReplies(parentPostNumber);
	    const scopedPostsByNumber = new Map(
	      posts.map((post) => [(0, import_identifiers.discoursePostReference)(post).postNumber, post])
	    ), knownRelationCount = this.#replies.topology.childrenOf(parentPostNumber).length, expectedCount = Math.max(
	      configuredExpected,
	      declaredExpected,
	      knownRelationCount,
	      posts.length
	    ), localArchive = this.#isLocalArchiveTopic();
	    if (localArchive || !options.refresh && expectedCount <= posts.length)
	      return Object.freeze({
	        parentPostNumber,
	        posts,
	        scopedPosts: posts,
	        expectedCount,
	        complete: expectedCount <= posts.length,
	        endpointExhausted: localArchive,
	        pageCount: 0,
	        nextAfter: 0
	      });
	    const request = this.#requests.loadNestedReplies;
	    if (typeof request != "function")
	      throw new Error("TopicSession 请求端口未提供 Discourse 直属回复能力");
	    const maxPages = Math.max(
	      1,
	      Math.min(100, positiveInteger(options.maxPages ?? 32, "maxPages"))
	    ), configuredMaxAttempts = options.maxAttempts === void 0 ? null : Math.max(
	      1,
	      Math.min(
	        4,
	        positiveInteger(options.maxAttempts, "maxAttempts")
	      )
	    ), maxAttempts = configuredMaxAttempts ?? 2;
	    let after = 0, pageCount = 0, endpointExhausted = !1;
	    const seenCursors = /* @__PURE__ */ new Set();
	    for (; pageCount < maxPages; ) {
	      this.#assertActive(), throwIfAborted(options.signal);
	      let payload, observedAt = this.#now(), lastError;
	      for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
	        throwIfAborted(options.signal), await awaitWithSignal(options.beforePage?.(), options.signal), this.#assertActive(), throwIfAborted(options.signal), observedAt = this.#now();
	        const activeRequest = Object.freeze({
	          parentPostId,
	          after,
	          background: profile.background,
	          ...options.refresh === void 0 ? {} : { refresh: options.refresh },
	          ...options.signal === void 0 ? {} : { signal: options.signal },
	          ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork }
	        });
	        profile.activeRequest = activeRequest;
	        try {
	          payload = await request.call(
	            this.#requests,
	            parentPostNumber,
	            activeRequest
	          ), lastError = void 0;
	          break;
	        } catch (error) {
	          if (lastError = error, options.signal?.aborted || isAbortFailure(error) || isAuthFailure(error) || isThrottleFailure(error)) throw error;
	          const allowedAttempts = configuredMaxAttempts ?? (profile.background ? 1 : 2);
	          if (attempt + 1 < allowedAttempts)
	            await awaitWithSignal(this.#wait(240), options.signal);
	          else
	            break;
	        } finally {
	          profile.activeRequest === activeRequest && (profile.activeRequest = null);
	        }
	      }
	      if (lastError !== void 0) throw lastError;
	      this.#assertActive(), pageCount += 1;
	      const pagePosts = discoursePostsFromPayload(payload), scopedPagePosts = pagePosts.map((post) => scopedReplyPost(post, parentPostNumber)).filter((post) => post !== null);
	      if (scopedPagePosts.length) {
	        throwIfAborted(options.signal), await awaitWithSignal(options.beforeCommit?.(), options.signal), this.#assertActive(), throwIfAborted(options.signal), this.ingestPosts(scopedPagePosts, "loader-batch", observedAt);
	        for (const post of scopedPagePosts)
	          scopedPostsByNumber.set(
	            (0, import_identifiers.discoursePostReference)(post).postNumber,
	            post
	          );
	        posts = this.#knownDirectReplies(parentPostNumber);
	      }
	      const nextAfter = replyPageCursor(pagePosts);
	      if (endpointExhausted = pagePosts.length === 0 || pagePosts.length < import_native_request_descriptors.DISCOURSE_DIRECT_REPLIES_PAGE_SIZE || nextAfter <= after || seenCursors.has(nextAfter), expectedCount <= scopedPostsByNumber.size || endpointExhausted) {
	        after = Math.max(after, nextAfter);
	        break;
	      }
	      seenCursors.add(nextAfter), after = nextAfter;
	    }
	    return Object.freeze({
	      parentPostNumber,
	      posts,
	      scopedPosts: Object.freeze([...scopedPostsByNumber.values()]),
	      expectedCount,
	      complete: expectedCount <= scopedPostsByNumber.size,
	      endpointExhausted,
	      pageCount,
	      nextAfter: after
	    });
	  }
	  #canonicalBranchContains(parentPostNumber, childPostNumber) {
	    const seen = /* @__PURE__ */ new Set();
	    let current = childPostNumber;
	    for (; current !== null && !seen.has(current); ) {
	      if (current === parentPostNumber) return !0;
	      seen.add(current);
	      const parent = this.#replies.topology.parentOf(current);
	      current = parent == null ? null : (0, import_identifiers.discoursePostReference)({ post_number: parent }).postNumber;
	    }
	    return !1;
	  }
	  #knownDirectReplies(parentPostNumber) {
	    return Object.freeze(
	      this.#replies.topology.childrenOf(parentPostNumber).map((postNumber) => this.#postByNumber.get(
	        (0, import_identifiers.discoursePostReference)({ post_number: postNumber }).postNumber
	      )).filter((post) => post !== void 0)
	    );
	  }
	  async #initialize(options) {
	    this.#assertActive(), await Promise.all([
	      this.#snapshots.restore(),
	      this.#replies.restore()
	    ]), this.#restoreIndexes(), await this.#prepareRestoredModerationHiddenPosts();
	    const snapshot = this.#snapshots.snapshot();
	    this.#authoritativeTopicExpectedPostCount = snapshot.expectedPostCount, this.#createdPostNumbersSinceAuthoritativeTopic.clear(), this.#replies.setExpectedPostCount(snapshot.expectedPostCount);
	    const cachedPosts = this.#snapshots.posts();
	    if (this.#replies.coverage().knownPostCount < cachedPosts.length) {
	      const repairedTree = this.#replies.ingest(
	        cachedPosts,
	        "loader-batch",
	        { observedAt: snapshot.updatedAt }
	      );
	      for (const error of repairedTree.listenerErrors) this.#onError(error);
	    }
	    const restoredStreamCount = this.#streamPostIds.reduce(
	      (count, postId) => count + (this.#postById.has(postId) ? 1 : 0),
	      0
	    ), archivedTopic = this.#snapshots.localArchiveState().topic !== null;
	    return this.#topic !== null && this.#streamPostIds.length > 0 && (archivedTopic || snapshot.expectedPostCount <= this.#streamPostIds.length) ? (this.#initializedFromCache = !0, this.#syncLocalArchiveState(), this.#onInitializeSource("cache", Object.freeze({
	      cachedCount: restoredStreamCount,
	      missingCount: 0,
	      totalCount: this.#streamPostIds.length
	    })), this.#refreshCachedInBackground && !this.#snapshots.isFresh() && this.refresh({ background: !0 }).then(() => this.#snapshots.localArchiveState().topic ? null : this.loadPostById(this.#streamPostIds.at(-1), {
	      background: !0
	    })).catch(this.#onError), this.#topic) : (this.#initializedFromCache = !1, this.#onInitializeSource("network", Object.freeze({
	      cachedCount: restoredStreamCount,
	      missingCount: Math.max(
	        1,
	        snapshot.expectedPostCount - restoredStreamCount
	      ),
	      totalCount: Math.max(
	        1,
	        snapshot.expectedPostCount,
	        this.#streamPostIds.length
	      )
	    })), this.#loadTopic(options, !1));
	  }
	  async #prepareRestoredModerationHiddenPosts() {
	    const currentPosts = this.cachedPosts(), prepared = await this.#prepareModerationHiddenPosts(currentPosts);
	    if (prepared === currentPosts) return;
	    const snapshot = this.#snapshots.snapshot(), archived = this.#snapshots.localArchiveState();
	    for (let index = 0; index < prepared.length; index += 1) {
	      const post = prepared[index];
	      if (post === currentPosts[index]) continue;
	      const postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber, stored = snapshot.posts.find((entry) => entry.postNumber === postNumber);
	      this.ingestPosts(
	        [post],
	        stored?.source ?? "loader-batch",
	        stored?.observedAt ?? snapshot.updatedAt
	      );
	      const marker = archived.posts.find((entry) => entry.postNumber === postNumber);
	      marker && (this.#snapshots.markPostUnavailable(
	        postNumber,
	        marker.status,
	        marker.confirmedAt
	      ), this.#unavailablePostNumbers.add(postNumber));
	    }
	    this.#syncLocalArchiveState();
	  }
	  #commitTopic(topic, source, observedAt, preparedPosts) {
	    const payloadTopicId = topic.id === void 0 ? this.topicId : (0, import_identifiers.discourseTopicId)(topic.id);
	    if (payloadTopicId !== this.topicId)
	      throw new Error(`Topic 响应 ${payloadTopicId} 与会话 ${this.topicId} 不一致`);
	    const streamPostIds = this.#mergeStreamPostIds(
	      (0, import_identifiers.discoursePostIdStream)(topic.post_stream?.stream ?? [])
	    ), posts = preparedPosts ?? discoursePostsFromPayload(topic), expectedPostCount = nonNegativeInteger(topic.posts_count);
	    return this.#commit(
	      { topic, posts, streamPostIds, expectedPostCount },
	      source,
	      observedAt
	    );
	  }
	  #commit(input, source, observedAt) {
	    const normalized = [];
	    let ignoredPosts = 0;
	    const byId = /* @__PURE__ */ new Map(), byNumber = /* @__PURE__ */ new Map();
	    for (const post of input.posts)
	      try {
	        const reference = (0, import_identifiers.discoursePostReference)(post);
	        if (reference.postId === null) throw new Error("Topic 楼层缺少 post.id");
	        if (reference.topicId !== null && reference.topicId !== this.topicId)
	          throw new Error(`楼层 #${reference.postNumber} 属于其他 Topic`);
	        const entry = Object.freeze({
	          post,
	          postId: reference.postId,
	          postNumber: reference.postNumber
	        }), previousById = byId.get(entry.postId);
	        previousById && (previousById.postNumber !== entry.postNumber && this.#onError(new Error(
	          `post.id ${entry.postId} 同时映射楼层 #${previousById.postNumber} 与 #${entry.postNumber}`
	        )), byNumber.delete(previousById.postNumber));
	        const previousByNumber = byNumber.get(entry.postNumber);
	        previousByNumber && (previousByNumber.postId !== entry.postId && this.#onError(new Error(
	          `楼层 #${entry.postNumber} 同时映射 post.id ${previousByNumber.postId} 与 ${entry.postId}`
	        )), byId.delete(previousByNumber.postId)), byId.set(entry.postId, entry), byNumber.set(entry.postNumber, entry);
	      } catch (error) {
	        ignoredPosts += 1, this.#onError(error);
	      }
	    normalized.push(...byId.values());
	    const posts = normalized.map((entry) => entry.post), treeResult = this.#replies.ingest(posts, source, { observedAt }), snapshotResult = this.#snapshots.ingest({
	      source,
	      observedAt,
	      ...input.topic === void 0 ? {} : { topic: input.topic },
	      ...input.streamPostIds === void 0 ? {} : { streamPostIds: input.streamPostIds },
	      ...input.expectedPostCount === void 0 ? {} : { expectedPostCount: input.expectedPostCount },
	      posts
	    });
	    input.expectedPostCount !== void 0 && this.#replies.setExpectedPostCount(
	      this.#snapshots.snapshot().expectedPostCount
	    ), input.topic !== void 0 && (this.#authoritativeTopicExpectedPostCount = this.#snapshots.snapshot().expectedPostCount, this.#createdPostNumbersSinceAuthoritativeTopic.clear()), this.#applySnapshotChanges(snapshotResult.changedPostNumbers), this.#syncLocalArchiveState();
	    const result = Object.freeze({
	      source,
	      observedAt,
	      acceptedPosts: snapshotResult.acceptedPosts,
	      ignoredPosts: ignoredPosts + snapshotResult.ignoredPosts,
	      changedPostNumbers: snapshotResult.changedPostNumbers,
	      topicChanged: snapshotResult.topicChanged,
	      streamChanged: snapshotResult.streamChanged
	    });
	    for (const error of treeResult.listenerErrors) this.#onError(error);
	    for (const error of this.changes.emit(result)) this.#onError(error);
	    return result;
	  }
	  /**
	   * 常规网络批次只会新增或刷新少量楼层;按仓储裁决后的 winner 增量更新索引,
	   * 避免全帖补流时每批重扫此前所有帖子。身份漂移或冲突属于异常输入,回退到
	   * 完整重建以保留既有冲突诊断与确定性 winner 语义。
	   */
	  #applySnapshotChanges(changedPostNumbers) {
	    if (this.#topic = this.#snapshots.topic(), this.#syncStreamPostIds(), !changedPostNumbers.length) return;
	    const entries = [], changedIds = /* @__PURE__ */ new Set();
	    let postStreamIndexChanged = !1;
	    for (const rawPostNumber of changedPostNumbers) {
	      const postNumber = (0, import_identifiers.discoursePostNumber)(rawPostNumber), post = this.#snapshots.post(postNumber);
	      if (!post) {
	        this.#restoreIndexes();
	        return;
	      }
	      try {
	        const reference = (0, import_identifiers.discoursePostReference)(post);
	        if (reference.postId === null || reference.postNumber !== postNumber || changedIds.has(reference.postId)) {
	          this.#restoreIndexes();
	          return;
	        }
	        const previousAtNumber = this.#postByNumber.get(postNumber);
	        if (previousAtNumber && (0, import_identifiers.discoursePostReference)(previousAtNumber).postId !== reference.postId) {
	          this.#restoreIndexes();
	          return;
	        }
	        const previousAtId = this.#postById.get(reference.postId);
	        if (previousAtId && (0, import_identifiers.discoursePostReference)(previousAtId).postNumber !== postNumber) {
	          this.#restoreIndexes();
	          return;
	        }
	        if (!!previousAtNumber != !!previousAtId) {
	          this.#restoreIndexes();
	          return;
	        }
	        previousAtNumber || (postStreamIndexChanged = !0), changedIds.add(reference.postId), entries.push(Object.freeze({
	          post,
	          postId: reference.postId,
	          postNumber
	        }));
	      } catch (error) {
	        this.#onError(error), this.#restoreIndexes();
	        return;
	      }
	    }
	    this.#cachedPostsSnapshot = null;
	    const archivedPostNumbers = new Set(
	      this.#snapshots.localArchiveState().posts.map((entry) => (0, import_identifiers.discoursePostNumber)(entry.postNumber))
	    );
	    for (const entry of entries)
	      this.#postById.set(entry.postId, entry.post), this.#postByNumber.set(entry.postNumber, entry.post), archivedPostNumbers.has(entry.postNumber) ? this.#unavailablePostNumbers.add(entry.postNumber) : this.#unavailablePostNumbers.delete(entry.postNumber);
	    postStreamIndexChanged && (this.#postStreamRevision += 1);
	  }
	  #restoreIndexes() {
	    this.#cachedPostsSnapshot = null, this.#topic = this.#snapshots.topic(), this.#syncStreamPostIds(), this.#postById.clear(), this.#postByNumber.clear();
	    const normalizedById = /* @__PURE__ */ new Map(), normalizedByNumber = /* @__PURE__ */ new Map();
	    for (const post of this.#snapshots.posts())
	      try {
	        const reference = (0, import_identifiers.discoursePostReference)(post);
	        if (reference.postId === null) continue;
	        const entry = Object.freeze({
	          post,
	          postId: reference.postId,
	          postNumber: reference.postNumber
	        }), previousById = normalizedById.get(entry.postId);
	        previousById && (previousById.postNumber !== entry.postNumber && this.#onError(new Error(
	          `快照 post.id ${entry.postId} 同时映射楼层 #${previousById.postNumber} 与 #${entry.postNumber}`
	        )), normalizedByNumber.delete(previousById.postNumber));
	        const previousByNumber = normalizedByNumber.get(entry.postNumber);
	        previousByNumber && (previousByNumber.postId !== entry.postId && this.#onError(new Error(
	          `快照楼层 #${entry.postNumber} 同时映射 post.id ${previousByNumber.postId} 与 ${entry.postId}`
	        )), normalizedById.delete(previousByNumber.postId)), normalizedById.set(entry.postId, entry), normalizedByNumber.set(entry.postNumber, entry);
	      } catch (error) {
	        this.#onError(error);
	      }
	    const archivedPostNumbers = new Set(
	      this.#snapshots.localArchiveState().posts.map((entry) => (0, import_identifiers.discoursePostReference)({ post_number: entry.postNumber }).postNumber)
	    );
	    for (const entry of normalizedById.values()) this.#postById.set(entry.postId, entry.post);
	    for (const entry of normalizedByNumber.values())
	      this.#postByNumber.set(entry.postNumber, entry.post);
	    for (const postNumber of this.#unavailablePostNumbers)
	      this.#postByNumber.has(postNumber) && !archivedPostNumbers.has(postNumber) && this.#unavailablePostNumbers.delete(postNumber);
	    for (const postNumber of archivedPostNumbers)
	      this.#unavailablePostNumbers.add(postNumber);
	    this.#postStreamRevision += 1;
	  }
	  #syncStreamPostIds() {
	    const next = this.#snapshots.streamPostIds();
	    if (next !== this.#streamPostIds) {
	      this.#streamPostIds = next, this.#postStreamRevision += 1, this.#streamIndexByPostId.clear();
	      for (let index = 0; index < next.length; index += 1)
	        this.#streamIndexByPostId.set(next[index], index);
	    }
	  }
	  #syncLocalArchiveState() {
	    const snapshot = this.#snapshots.localArchiveState(), key = JSON.stringify([
	      snapshot.topic?.status ?? 0,
	      snapshot.topic?.confirmedAt ?? 0,
	      snapshot.posts.map((entry) => [
	        entry.postNumber,
	        entry.status,
	        entry.confirmedAt
	      ])
	    ]);
	    if (key !== this.#archiveStateKey) {
	      this.#archiveStateKey = key;
	      for (const error of this.archiveChanges.emit(snapshot)) this.#onError(error);
	    }
	  }
	  #isLocalArchiveTopic() {
	    return this.#snapshots.localArchiveState().topic !== null;
	  }
	  #mergeStreamPostIds(incoming) {
	    if (!this.#streamPostIds.length) return incoming;
	    const merged = [...incoming], known = new Set(merged);
	    for (const postId of this.#streamPostIds) {
	      if (known.has(postId)) continue;
	      const post = this.#postById.get(postId);
	      if (!post) {
	        merged.push(postId), known.add(postId);
	        continue;
	      }
	      const postNumber = (0, import_identifiers.discoursePostReference)(post).postNumber;
	      let insertAt = merged.length;
	      for (let index = 0; index < merged.length; index += 1) {
	        const candidate = this.#postById.get(merged[index]);
	        if (candidate && (0, import_identifiers.discoursePostReference)(candidate).postNumber > postNumber) {
	          insertAt = index;
	          break;
	        }
	      }
	      merged.splice(insertAt, 0, postId), known.add(postId);
	    }
	    return Object.freeze(merged);
	  }
	  #streamWithCreatedPost(postId, postNumber) {
	    if (this.#streamPostIds.includes(postId)) return;
	    const mutable = [...this.#streamPostIds];
	    let insertAt = mutable.length;
	    for (; insertAt > 0; ) {
	      const previous = this.#postById.get(mutable[insertAt - 1]);
	      if (!previous || (0, import_identifiers.discoursePostReference)(previous).postNumber <= postNumber) break;
	      insertAt -= 1;
	    }
	    return mutable.splice(insertAt, 0, postId), Object.freeze(mutable);
	  }
	  async #ensurePostIds(missing, options) {
	    if (options.ingestSource === "target-refresh") {
	      await this.#fetchPostBatch(missing, options, 0);
	      return;
	    }
	    options.background !== !0 && this.#promotePendingPostBatches(options);
	    const unclaimed = missing.filter((postId) => !this.#pendingByPostId.has(postId));
	    if (unclaimed.length) {
	      const request = this.#fetchPostBatch(unclaimed, options, 0).finally(() => {
	        for (const postId of unclaimed)
	          this.#pendingByPostId.get(postId) === request && this.#pendingByPostId.delete(postId);
	      });
	      for (const postId of unclaimed) this.#pendingByPostId.set(postId, request);
	    }
	    await Promise.all([...new Set(
	      missing.map((postId) => this.#pendingByPostId.get(postId)).filter(
	        (request) => request !== void 0
	      )
	    )]);
	  }
	  #promotePendingPostBatches(options) {
	    if (!this.#requests.promotePostsByIds) return;
	    const claimed = new Set(this.#pendingByPostId.values());
	    for (const request of claimed) {
	      const batch = [...this.#pendingByPostId.entries()].filter(([, pending]) => pending === request).map(([postId]) => postId);
	      batch.length && this.#requests.promotePostsByIds(batch, options);
	    }
	  }
	  async #fetchPostBatch(postIds, options, splitDepth) {
	    const observedAt = this.#now();
	    try {
	      const payload = await this.#requests.loadPostsByIds(postIds, {
	        ...options.background === void 0 ? {} : { background: options.background },
	        ...options.refresh === void 0 ? {} : { refresh: options.refresh },
	        ...options.priority === void 0 ? {} : { priority: options.priority },
	        ...options.prefetchTier === void 0 ? {} : { prefetchTier: options.prefetchTier },
	        ...options.beforeNetwork === void 0 ? {} : { beforeNetwork: options.beforeNetwork }
	      }), posts = discoursePostsFromPayload(payload);
	      await options.beforeCommit?.(), this.#assertActive(), this.ingestPosts(
	        posts,
	        options.ingestSource ?? "loader-batch",
	        observedAt
	      );
	    } catch (error) {
	      if (shouldSplit(error) && postIds.length > 1 && splitDepth < 1) {
	        const middle = Math.ceil(postIds.length / 2), failure = (await Promise.allSettled([
	          this.#fetchPostBatch(postIds.slice(0, middle), options, splitDepth + 1),
	          this.#fetchPostBatch(postIds.slice(middle), options, splitDepth + 1)
	        ])).find((result) => result.status === "rejected");
	        if (failure) throw failure.reason;
	        return;
	      }
	      throw error;
	    }
	  }
	  async #loadRelative(rawPostNumber, direction, options) {
	    if (this.#assertActive(), !this.#streamPostIds.length) return Object.freeze([]);
	    const postNumber = (0, import_identifiers.discoursePostReference)({ post_number: rawPostNumber }).postNumber, last = direction === "last", targetIndex = last ? this.#streamPostIds.length - 1 : this.#streamIndexForPostNumber(postNumber);
	    if (direction === "before" && targetIndex <= 0) return Object.freeze([]);
	    const start = last ? targetIndex : direction === "before" ? Math.max(0, targetIndex - this.#pageSize) : Math.min(this.#streamPostIds.length, targetIndex + 1), end = last ? this.#streamPostIds.length : direction === "before" ? targetIndex : start + this.#pageSize, result = await this.loadPostsByIds(this.#streamPostIds.slice(start, end), options);
	    return last && result.posts.length && this.#advanceCursorPast(result.posts), result.posts;
	  }
	  #streamIndexForPostNumber(postNumber) {
	    const post = this.#postByNumber.get(postNumber);
	    if (post) {
	      const reference = (0, import_identifiers.discoursePostReference)(post);
	      if (reference.postId !== null) {
	        const exact = this.#streamIndexByPostId.get(reference.postId);
	        if (exact !== void 0) return exact;
	      }
	    }
	    let bestIndex = -1, bestDistance = Number.POSITIVE_INFINITY;
	    for (const [knownPostNumber, knownPost] of this.#postByNumber) {
	      let knownIndex;
	      try {
	        const knownPostId = (0, import_identifiers.discoursePostReference)(knownPost).postId;
	        knownPostId !== null && (knownIndex = this.#streamIndexByPostId.get(knownPostId));
	      } catch {
	        continue;
	      }
	      if (knownIndex === void 0) continue;
	      const distance = Math.abs(knownPostNumber - postNumber);
	      (distance < bestDistance || distance === bestDistance && knownPostNumber <= postNumber) && (bestDistance = distance, bestIndex = knownIndex + (postNumber - knownPostNumber));
	    }
	    return Math.min(
	      this.#streamPostIds.length - 1,
	      Math.max(0, bestIndex >= 0 ? bestIndex : postNumber - 1)
	    );
	  }
	  #advanceCursorPast(posts, targetPostNumber) {
	    let maxIndex = -1;
	    for (const post of posts)
	      try {
	        const reference = (0, import_identifiers.discoursePostReference)(post);
	        reference.postId !== null && (maxIndex = Math.max(maxIndex, this.#streamPostIds.indexOf(reference.postId)));
	      } catch {
	      }
	    maxIndex < 0 && targetPostNumber !== void 0 && (maxIndex = Math.min(
	      this.#streamPostIds.length - 1,
	      Math.max(0, targetPostNumber - 1)
	    )), maxIndex >= this.#cursor && (this.#cursor = maxIndex + 1);
	  }
	  #batchResult(posts, done, missingPostIds, retry, fatal, error) {
	    return Object.freeze({
	      posts: Object.freeze([...posts]),
	      done,
	      retry,
	      fatal,
	      ...error === void 0 ? {} : { error },
	      missingPostIds: freezeNumbers(missingPostIds)
	    });
	  }
	  #assertActive() {
	    if (this.#closed || this.scope.destroyed || this.#signal?.aborted)
	      throw this.#signal?.aborted ? this.#signal.reason ?? new DOMException("Topic session closed", "AbortError") : new DOMException("Topic session closed", "AbortError");
	  }
	}
}, "6b8e03f75992923fa40f092c611198e24931f6f8b82fc33a58e5261f90193f09");

/* Source: lite/src/userscript/browser-share-surface.ts */
runtime.register("src/userscript/browser-share-surface.js", function(module, exports, require) {
	var browser_share_surface_exports = {};
	__export(browser_share_surface_exports, {
	  BrowserReaderShareSurface: () => BrowserReaderShareSurface
	});
	module.exports = __toCommonJS(browser_share_surface_exports);
	var import_value_record = require("../kernel/value-record.js");
	class BrowserReaderShareSurface {
	  #navigator;
	  constructor(pageWindow) {
	    const page = (0, import_value_record.valueRecord)(pageWindow);
	    this.#navigator = (0, import_value_record.valueRecord)(page?.navigator) ?? Object.freeze({});
	  }
	  async share(input) {
	    const share = this.#navigator.share;
	    if (typeof share != "function") return "unsupported";
	    try {
	      return await share.call(this.#navigator, input), "shared";
	    } catch (cause) {
	      if ((0, import_value_record.valueRecord)(cause)?.name === "AbortError") return "cancelled";
	      throw cause;
	    }
	  }
	  async copyText(text) {
	    const clipboard = (0, import_value_record.valueRecord)(this.#navigator.clipboard), writeText = clipboard?.writeText;
	    if (typeof writeText != "function")
	      throw new Error("浏览器剪贴板不可用");
	    await writeText.call(clipboard, text);
	  }
	}
}, "ea7818f3373ea5d34e4251de1fa84d0f8d0f136ac4484637bb53967e9486b9d8");

/* Source: lite/src/userscript/browser-userscript-environment.ts */
runtime.register("src/userscript/browser-userscript-environment.js", function(module, exports, require) {
	var browser_userscript_environment_exports = {};
	__export(browser_userscript_environment_exports, {
	  BrowserUserscriptEnvironment: () => BrowserUserscriptEnvironment
	});
	module.exports = __toCommonJS(browser_userscript_environment_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_coordinated_request_client = require("../network/coordinated-request-client.js"), import_discourse_native_read_transport = require("../network/discourse-native-read-transport.js"), import_public_resource_request_adapter = require("../network/public-resource-request-adapter.js"), import_reader_search = require("../search/reader-search.js"), import_translation_request_adapter = require("../translation/translation-request-adapter.js"), import_browser_share_surface = require("./browser-share-surface.js"), import_browser_discourse_site_probe = require("../site/browser-discourse-site-probe.js"), import_value_record = require("../kernel/value-record.js"), import_reader_webdav_client = require("../sync/reader-webdav-client.js");
	class BrowserUserscriptEnvironment {
	  #userscriptGlobal;
	  pageWindow;
	  discourseHost;
	  constructor(options) {
	    const userscriptGlobal = (0, import_value_record.objectRecord)(options.userscriptGlobal);
	    if (!userscriptGlobal) throw new Error("userscript global 不可用");
	    const pageWindow = userscriptGlobal.unsafeWindow ?? userscriptGlobal.window;
	    if (!(0, import_value_record.objectRecord)(pageWindow))
	      throw new Error("userscript page window 不可用");
	    this.#userscriptGlobal = userscriptGlobal, this.pageWindow = pageWindow, this.discourseHost = new import_native_host_api.BrowserDiscourseHostApiPort({ pageWindow });
	  }
	  async waitForDiscourseRuntime(signal, options = {}) {
	    const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? 15e3)), pollIntervalMs = Math.max(
	      1,
	      Math.floor(options.pollIntervalMs ?? 50)
	    ), now = options.now ?? Date.now, delay = options.delay ?? import_coordinated_request_client.abortableDelay, startedAt = now();
	    for (; !(0, import_discourse_native_read_transport.discourseNativeAjaxAvailable)(this.discourseHost) || !(0, import_native_host_api.discourseNativeCurrentUserBindingAvailable)(this.discourseHost); ) {
	      if (signal.aborted) throw signal.reason;
	      const remaining = timeoutMs - (now() - startedAt);
	      if (remaining <= 0)
	        throw new Error(
	          "Discourse 原生 Ajax/current-user 在启动期限内未就绪"
	        );
	      await delay(Math.min(pollIntervalMs, remaining), signal);
	    }
	  }
	  createExternalHttp(options = {}) {
	    const rawRequest = this.#userscriptGlobal.GM_xmlhttpRequest;
	    if (typeof rawRequest != "function")
	      throw new Error("GM_xmlhttpRequest 不可用,无法访问登记的外部服务");
	    const request = (requestOptions) => rawRequest.call(this.#userscriptGlobal, requestOptions);
	    return new import_translation_request_adapter.BrowserUserscriptExternalHttpPort({
	      ...options,
	      request
	    });
	  }
	  createDiscourseSiteProbe() {
	    const rawRequest = this.#userscriptGlobal.GM_xmlhttpRequest;
	    if (typeof rawRequest != "function")
	      throw new Error("GM_xmlhttpRequest 不可用,无法检测自定义站点");
	    const request = (requestOptions) => rawRequest.call(this.#userscriptGlobal, requestOptions);
	    return new import_browser_discourse_site_probe.BrowserDiscourseSiteProbe({ request });
	  }
	  createWebDavClient() {
	    const rawRequest = this.#userscriptGlobal.GM_xmlhttpRequest;
	    if (typeof rawRequest != "function")
	      throw new Error("GM_xmlhttpRequest 不可用,无法访问 WebDAV");
	    const request = (requestOptions) => rawRequest.call(this.#userscriptGlobal, requestOptions);
	    return new import_reader_webdav_client.ReaderWebDavClient({ request });
	  }
	  createPublicResourceHttp() {
	    const rawRequest = (0, import_value_record.objectRecord)(this.pageWindow)?.fetch;
	    if (typeof rawRequest != "function")
	      throw new Error("page window fetch 不可用,无法访问公共图片资源");
	    const request = (input, init) => rawRequest.call(this.pageWindow, input, init);
	    return new import_public_resource_request_adapter.BrowserPublicResourceHttpPort({ request });
	  }
	  createCreditBridgeHttp() {
	    const rawRequest = (0, import_value_record.objectRecord)(this.pageWindow)?.fetch;
	    if (typeof rawRequest != "function")
	      throw new Error("page window fetch 不可用,无法读取 LDC 账户摘要");
	    return Object.freeze({
	      loadUserInfo: async (signal) => {
	        const response = await rawRequest.call(
	          this.pageWindow,
	          "/api/v1/oauth/user-info",
	          {
	            signal,
	            credentials: "include",
	            cache: "no-store",
	            headers: { Accept: "application/json" }
	          }
	        );
	        if (!response.ok) throw new Error(`LDC HTTP ${response.status}`);
	        return response.json();
	      }
	    });
	  }
	  createObjectUrlPort() {
	    const urlOwner = (0, import_value_record.objectRecord)(this.pageWindow)?.URL, create = urlOwner?.createObjectURL, revoke = urlOwner?.revokeObjectURL;
	    if (typeof create != "function" || typeof revoke != "function")
	      throw new Error("page window URL.createObjectURL/revokeObjectURL 不可用");
	    return Object.freeze({
	      createObjectURL: (blob) => String(create.call(urlOwner, blob)),
	      revokeObjectURL: (source) => {
	        revoke.call(urlOwner, source);
	      }
	    });
	  }
	  createAssetCacheStorage() {
	    const page = (0, import_value_record.objectRecord)(this.pageWindow), storage = (0, import_value_record.objectRecord)(page?.caches), keys = storage?.keys, open = storage?.open, remove = storage?.delete;
	    return typeof keys != "function" || typeof open != "function" || typeof remove != "function" ? null : Object.freeze({
	      keys: () => keys.call(storage),
	      open: (name) => open.call(storage, name),
	      delete: (name) => remove.call(storage, name)
	    });
	  }
	  createValueStorage() {
	    const modern = (0, import_value_record.objectRecord)(this.#userscriptGlobal.GM), modernGet = modern?.getValue, modernSet = modern?.setValue;
	    if (typeof modernGet == "function" && typeof modernSet == "function")
	      return Object.freeze({
	        getValue: (key) => modernGet.call(modern, key, null),
	        setValue: (key, value) => modernSet.call(modern, key, value)
	      });
	    const legacyGet = this.#userscriptGlobal.GM_getValue, legacySet = this.#userscriptGlobal.GM_setValue;
	    return typeof legacyGet != "function" || typeof legacySet != "function" ? null : Object.freeze({
	      getValue: (key) => legacyGet.call(this.#userscriptGlobal, key, null),
	      setValue: (key, value) => legacySet.call(this.#userscriptGlobal, key, value)
	    });
	  }
	  createShareSurface() {
	    return new import_browser_share_surface.BrowserReaderShareSurface(this.pageWindow);
	  }
	  createKatexPort() {
	    const page = (0, import_value_record.objectRecord)(this.pageWindow), owner = (0, import_value_record.objectRecord)(this.#userscriptGlobal.katex) ?? (0, import_value_record.objectRecord)(page?.katex), render = owner?.render;
	    return typeof render != "function" ? null : Object.freeze({
	      render: (tex, target, options) => {
	        render.call(owner, tex, target, options);
	      }
	    });
	  }
	  createHlsPort() {
	    const page = (0, import_value_record.objectRecord)(this.pageWindow), candidate = this.#userscriptGlobal.Hls ?? page?.Hls, isSupported = candidate?.isSupported;
	    return typeof candidate != "function" || typeof isSupported != "function" ? null : Object.freeze({
	      isSupported: () => !!isSupported.call(candidate),
	      create: () => {
	        const player = new candidate(), loadSource = player.loadSource, attachMedia = player.attachMedia, destroy = player.destroy;
	        if (typeof loadSource != "function" || typeof attachMedia != "function" || typeof destroy != "function")
	          throw new Error("Hls player 缺少标准生命周期方法");
	        return Object.freeze({
	          loadSource: (source) => {
	            loadSource.call(player, source);
	          },
	          attachMedia: (video) => {
	            attachMedia.call(player, video);
	          },
	          destroy: () => {
	            destroy.call(player);
	          }
	        });
	      }
	    });
	  }
	  readScriptVersion() {
	    const modern = (0, import_value_record.objectRecord)(this.#userscriptGlobal.GM), info = (0, import_value_record.objectRecord)(modern?.info) ?? (0, import_value_record.objectRecord)(this.#userscriptGlobal.GM_info), script = (0, import_value_record.objectRecord)(info?.script);
	    return String(script?.version ?? "").trim() || null;
	  }
	  async readTextResource(name) {
	    const resourceName = String(name).trim();
	    if (!resourceName) throw new Error("userscript resource name 不能为空");
	    const modern = (0, import_value_record.objectRecord)(this.#userscriptGlobal.GM), modernRead = modern?.getResourceText, legacyRead = this.#userscriptGlobal.GM_getResourceText, value = typeof modernRead == "function" ? await modernRead.call(modern, resourceName) : typeof legacyRead == "function" ? await legacyRead.call(this.#userscriptGlobal, resourceName) : null;
	    if (typeof value != "string" || !value.trim())
	      throw new Error(
	        `userscript 文本资源 ${resourceName} 不可用`
	      );
	    return value;
	  }
	  createPinyinSearchForms(maxEntries = 512) {
	    const limit = Math.floor(Number(maxEntries));
	    if (!Number.isSafeInteger(limit) || limit <= 0)
	      throw new RangeError("拼音搜索缓存上限必须是正整数");
	    const page = (0, import_value_record.objectRecord)(this.pageWindow), pinyinOwner = (0, import_value_record.objectRecord)(this.#userscriptGlobal.pinyinPro) ?? (0, import_value_record.objectRecord)(page?.pinyinPro), pinyin = pinyinOwner?.pinyin, cache = /* @__PURE__ */ new Map();
	    return (value) => {
	      const source = String(value ?? "");
	      if (!source) return Object.freeze([]);
	      const cached = cache.get(source);
	      if (cached)
	        return cache.delete(source), cache.set(source, cached), cached;
	      const forms = [(0, import_reader_search.normalizeReaderSearchText)(source)];
	      if (typeof pinyin == "function")
	        try {
	          forms.push((0, import_reader_search.normalizeReaderSearchText)(pinyin.call(pinyinOwner, source, {
	            toneType: "none",
	            nonZh: "consecutive"
	          }))), forms.push((0, import_reader_search.normalizeReaderSearchText)(pinyin.call(pinyinOwner, source, {
	            pattern: "first",
	            toneType: "none",
	            nonZh: "consecutive"
	          })));
	        } catch {
	        }
	      const result = Object.freeze([...new Set(forms.filter(Boolean))]);
	      for (cache.set(source, result); cache.size > limit; ) {
	        const oldest = cache.keys().next().value;
	        if (oldest === void 0) break;
	        cache.delete(oldest);
	      }
	      return result;
	    };
	  }
	}
}, "6f3de165942d7bf43142e7f3b938030155c244dcf1c8514c80a5917e0fb3329a");

/* Source: lite/src/userscript/main-lite-bootstrap.ts */
runtime.register("src/userscript/main-lite-bootstrap.js", function(module, exports, require) {
	var main_lite_bootstrap_exports = {};
	__export(main_lite_bootstrap_exports, {
	  startMainLiteUserscript: () => startMainLiteUserscript,
	  startMianLiteUserscript: () => startMianLiteUserscript
	});
	module.exports = __toCommonJS(main_lite_bootstrap_exports);
	var import_reader_application_stages = require("../app/reader-application-stages.js"), import_native_host_api = require("../discourse/native-host-api.js"), import_reader_native_composer_window = require("../discourse/reader-native-composer-window.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_history_model = require("../history/reader-history-model.js"), import_reader_katex_controller = require("../media/reader-katex-controller.js"), import_reader_image_preferences = require("../media/reader-image-preferences.js"), import_reader_appearance_style_controller = require("../appearance/reader-appearance-style-controller.js"), import_reader_theme_controller = require("../appearance/reader-theme-controller.js"), import_reader_local_sun_clock = require("../appearance/reader-local-sun-clock.js"), import_reader_font_style_controller = require("../font/reader-font-style-controller.js"), import_reader_layout_style_controller = require("../layout/reader-layout-style-controller.js"), import_reader_shell_template = require("../shell/reader-shell-template.js"), import_reader_surface_portal = require("../shell/reader-surface-portal.js"), import_reader_shortcut_controller = require("../shell/reader-shortcut-controller.js"), import_reader_workspace = require("../shell/reader-workspace.js"), import_embedded_host_topic_card_enhancement = require("../shell/embedded-host-topic-card-enhancement.js"), import_reader_motion_settings_form = require("../settings/reader-motion-settings-form.js"), import_boost_copy_rule = require("../post/boost-copy-rule.js"), import_reader_topic_action_rail = require("../post/reader-topic-action-rail.js"), import_reader_unwanted_topic_filter = require("../collection/reader-unwanted-topic-filter.js"), import_reader_performance_settings_form = require("../settings/reader-performance-settings-form.js"), import_reader_reading_settings_form = require("../settings/reader-reading-settings-form.js"), import_reader_open_queue_session = require("../queue/reader-open-queue-session.js"), import_reader_settings_reset_reminder = require("../settings/reader-settings-reset-reminder.js"), import_read_viewport_adapter = require("../reading/read-viewport-adapter.js"), import_reader_preferences_schema = require("../state/reader-preferences-schema.js"), import_reader_post_presentation = require("../topic/reader-post-presentation.js"), import_translation_text = require("../translation/translation-text.js"), import_reader_translation_config = require("../translation/reader-translation-config.js"), import_reader_reply_tree_preferences = require("../topic/reader-reply-tree-preferences.js"), import_browser_userscript_environment = require("./browser-userscript-environment.js"), import_reader_userscript_application = require("./reader-userscript-application.js"), import_reader_userscript_target_adapter = require("./reader-userscript-target-adapter.js"), import_reader_native_topic_route = require("../topic/reader-native-topic-route.js"), import_reader_credit_account_bridge = require("../user/reader-credit-account-bridge.js"), import_reader_custom_site_repository = require("../site/reader-custom-site-repository.js"), import_reader_embedded_reload_coordinator = require("./reader-embedded-reload-coordinator.js"), import_browser_shared_request_permit = require("../network/browser-shared-request-permit.js"), import_reader_webdav_config_repository = require("../sync/reader-webdav-config-repository.js");
	const DEBUG_HANDLE_KEY = "__LDP_MAIN_LITE__", LEGACY_DEBUG_HANDLE_KEY = "__LDP_MIAN_LITE__", CHALLENGE_MONITOR_KEY = "__LDP_CLOUDFLARE_CHALLENGE_MONITOR__", STYLE_ID = "ldp-mian-lite-styles", STYLE_RESOURCE = "ldpReaderStyles", KATEX_STYLE_RESOURCE = "ldpKatexStyles", KATEX_STYLESHEET_URL = "https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css";
	function pageRecord(value) {
	  if (value === null || typeof value != "object" && typeof value != "function")
	    throw new Error("main-lite page window 不可用");
	  return value;
	}
	function theme(document, window) {
	  return document.documentElement.classList.contains("dark") || document.documentElement.dataset.colorScheme === "dark" || window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
	}
	function sourceId(window) {
	  const value = window.crypto?.randomUUID?.();
	  return value ? `main-lite:${value}` : `main-lite:${Date.now()}`;
	}
	function createStyleStage(environment, document, state) {
	  return Object.freeze({
	    name: "userscript-styles",
	    required: !0,
	    async setup() {
	      const [css, katexCss] = await Promise.all([
	        environment.readTextResource(STYLE_RESOURCE),
	        environment.readTextResource(KATEX_STYLE_RESOURCE)
	      ]);
	      document.getElementById(STYLE_ID)?.remove();
	      const style = document.createElement("style");
	      style.id = STYLE_ID;
	      const stylesheet = `${css}
${(0, import_reader_katex_controller.readerKatexStylesheet)(
	        katexCss,
	        KATEX_STYLESHEET_URL
	      )}`;
	      style.textContent = stylesheet;
	      let portal = null;
	      try {
	        portal = new import_reader_surface_portal.ReaderSurfacePortal(document, stylesheet), (document.head ?? document.documentElement).append(style);
	      } catch (cause) {
	        throw portal?.destroy(), style.remove(), cause;
	      }
	      return state.portal = portal, () => {
	        portal.destroy(), state.portal === portal && (state.portal = null), style.remove();
	      };
	    }
	  });
	}
	function requestedMode(preferences, routeKind) {
	  return routeKind === "direct-topic" ? preferences.topicReaderMode : preferences.listReaderMode;
	}
	function createRuntimeStage(environment, document, window, state, serviceWorkerMessages, suppressInitialTopicOpen, customSites) {
	  let template = null;
	  const origin = document.location.origin, siteName = (0, import_reader_custom_site_repository.readerDiscourseSiteDisplayName)(document.location.hostname), routeKind = (0, import_reader_userscript_target_adapter.readerUserscriptRouteKind)(
	    document.location.href,
	    origin
	  );
	  return Object.freeze({
	    name: "reader-userscript-runtime",
	    required: !0,
	    async setup(scope, applicationContext) {
	      const readiness = scope.abortController(
	        new DOMException("main-lite runtime 启动已取消", "AbortError")
	      );
	      await environment.waitForDiscourseRuntime(readiness.signal);
	      const initialPreferences = applicationContext.readPreferences();
	      let defaultSiteThemeReloadRequested = !1;
	      const enforceEmbeddedDefaultSiteTheme = (mode) => {
	        if (!(routeKind !== "list" || (0, import_reader_workspace.readerWorkspacePositionMode)(mode) !== "embedded" || defaultSiteThemeReloadRequested || (0, import_native_host_api.discourseNativeDefaultSiteTheme)(
	          environment.discourseHost
	        ) !== "updated")) {
	          defaultSiteThemeReloadRequested = !0;
	          try {
	            window.location.reload();
	          } catch (cause) {
	            console.error("[main-lite:default-site-theme]", cause);
	          }
	        }
	      };
	      enforceEmbeddedDefaultSiteTheme(
	        requestedMode(initialPreferences, routeKind)
	      );
	      const replyTreePreferences = new import_reader_reply_tree_preferences.ReaderReplyTreePreferencesPreview(
	        import_reader_reply_tree_preferences.readerPreferencesReplyTreeAdapter.read(initialPreferences),
	        (error) => {
	          console.error("[main-lite:reply-tree-preview]", error);
	        }
	      );
	      scope.add(() => replyTreePreferences.destroy());
	      const preferencesEnvironment = Object.freeze({
	        viewportWidth: window.innerWidth,
	        viewportHeight: window.innerHeight
	      }), scriptVersion = environment.readScriptVersion() ?? "development", preferencesCodec = (0, import_reader_preferences_schema.createReaderPreferencesConfigCodec)({
	        environment: preferencesEnvironment,
	        scriptVersion
	      }), preferencesDefaults = (0, import_reader_preferences_schema.createReaderPreferencesDefaults)(
	        preferencesEnvironment
	      ), currentUsername = (0, import_native_host_api.discourseNativeInitialCurrentUsername)(
	        environment.discourseHost
	      ), renderNativeIcon = (0, import_native_host_api.discourseNativeIconRenderer)(
	        environment.discourseHost
	      ), siteLogoCandidates = [
	        document.querySelector(
	          ".d-header #site-logo,.d-header img.logo-big,.d-header img.logo-small,.custom-logo-link img"
	        )?.currentSrc,
	        document.querySelector(
	          'link[rel~="apple-touch-icon"]'
	        )?.href,
	        document.querySelector(
	          'link[rel~="icon"]'
	        )?.href
	      ].filter((value) => !!value), siteLogoUrl = (0, import_native_host_api.discourseNativeSiteLogoUrl)(
	        environment.discourseHost,
	        origin,
	        siteLogoCandidates
	      ), renderIcon = (name, iconDocument) => {
	        const nativeIcon = renderNativeIcon(name, iconDocument);
	        return (0, import_reader_icon.resolveReaderIcon)(iconDocument, name, nativeIcon);
	      }, katex = environment.createKatexPort(), hls = environment.createHlsPort(), bodyTranslationAllowed = (0, import_reader_custom_site_repository.readerDiscourseSiteAllowsBodyTranslation)(
	        document.location.hostname
	      );
	      let persistTranslationMode = null, hostTopicEnhancement = null;
	      const unwantedTopicFilter = Object.freeze({
	        read: () => import_reader_unwanted_topic_filter.readerPreferencesUnwantedTopicFilterAdapter.read(
	          applicationContext.readPreferences()
	        ),
	        update: (preferences) => {
	          if (!applicationContext.updatePreferences)
	            throw new Error("当前环境不能保存自动过滤设置");
	          applicationContext.updatePreferences(
	            import_reader_unwanted_topic_filter.readerPreferencesUnwantedTopicFilterAdapter.createPatch(
	              preferences
	            )
	          );
	        },
	        subscribe: (listener, preferenceScope) => {
	          let previous = import_reader_unwanted_topic_filter.readerPreferencesUnwantedTopicFilterAdapter.read(
	            applicationContext.readPreferences()
	          );
	          return applicationContext.preferenceChanges.subscribe(
	            (preferences) => {
	              const next = import_reader_unwanted_topic_filter.readerPreferencesUnwantedTopicFilterAdapter.read(
	                preferences
	              );
	              (0, import_reader_unwanted_topic_filter.readerUnwantedTopicFilterPreferencesEqual)(
	                previous,
	                next
	              ) || (previous = next, listener(next));
	            },
	            preferenceScope
	          );
	        }
	      });
	      return (0, import_reader_userscript_application.createReaderUserscriptRuntimeStage)({
	        environment,
	        shell: {
	          compatibilityKey: () => `${origin}:mian-lite:v1`,
	          createView: () => {
	            const portal = state.portal;
	            if (!portal) throw new Error("main-lite Shadow Portal 未就绪");
	            return template = (0, import_reader_shell_template.createReaderShellTemplate)({
	              document,
	              mount: portal.root,
	              listModeAllowed: routeKind === "list",
	              siteName,
	              homeUrl: `${origin}/`,
	              logoUrl: siteLogoUrl,
	              renderIcon
	            }), template.view;
	          },
	          createWorkspaceOptions: (_shell, context) => {
	            if (!template) throw new Error("main-lite Shell template 未创建");
	            const readPreferences = context.readPreferences, scrolling = () => document.scrollingElement ?? document.documentElement;
	            return hostTopicEnhancement = new import_embedded_host_topic_card_enhancement.EmbeddedHostTopicCardEnhancement(
	              document,
	              environment.discourseHost,
	              {
	                openedTopicStorage: window.localStorage,
	                openedTopicStorageScope: currentUsername,
	                isTopicHidden: (topicId) => state.runtime?.unwantedTopics.has(topicId) === !0,
	                hideTopic: (input) => {
	                  const runtime = state.runtime;
	                  if (!runtime) throw new Error("不想看仓库尚未就绪");
	                  runtime.unwantedTopics.remember(input);
	                },
	                automaticFilter: (input) => state.runtime ? (0, import_reader_unwanted_topic_filter.readerUnwantedTopicFilterMatch)(
	                  import_reader_unwanted_topic_filter.readerPreferencesUnwantedTopicFilterAdapter.read(
	                    applicationContext.readPreferences()
	                  ),
	                  input
	                ) : null,
	                notify: (message) => state.runtime?.feedback.show(message),
	                onError: (cause) => {
	                  console.error(
	                    "[main-lite:host-topic-notification]",
	                    cause
	                  );
	                }
	              }
	            ), {
	              document,
	              routeKind,
	              requestedMode: requestedMode(
	                readPreferences(),
	                routeKind
	              ),
	              embedWidth: readPreferences().listReaderEmbedWidth,
	              windowPreferences: readPreferences(),
	              topicFilterChanges: unwantedTopicFilter,
	              elements: template.workspaceElements,
	              viewportTarget: window,
	              pointerTarget: document,
	              scrollTarget: window,
	              readViewport: () => ({
	                width: window.innerWidth,
	                height: window.innerHeight
	              }),
	              hostScroll: {
	                read: () => ({
	                  viewportHeight: window.innerHeight,
	                  scrollHeight: scrolling().scrollHeight,
	                  scrollTop: scrolling().scrollTop
	                }),
	                readScrollTop: () => scrolling().scrollTop,
	                scrollTo: (top) => window.scrollTo({
	                  top,
	                  behavior: "auto"
	                })
	              },
	              enhancements: hostTopicEnhancement,
	              readAppearance: () => {
	                const preferences = readPreferences(), activeTheme = theme(document, window);
	                return {
	                  profile: preferences.appearanceProfile,
	                  theme: activeTheme,
	                  defaultDividerLineColor: activeTheme === "dark" ? "#343b44" : "#e5e5e5",
	                  defaultDividerLineWidth: 0.5
	                };
	              },
	              onPersistMode: (mode) => {
	                context.updatePreferences?.(
	                  routeKind === "direct-topic" ? {
	                    topicReaderMode: mode === "fullpage" ? "fullpage" : "floating"
	                  } : { listReaderMode: mode }
	                ), enforceEmbeddedDefaultSiteTheme(mode);
	              },
	              onPersistEmbedWidth: (listReaderEmbedWidth) => {
	                context.updatePreferences?.({ listReaderEmbedWidth });
	              },
	              onPersistWindow: (preferences) => {
	                context.updatePreferences?.({
	                  readerWindowWidth: preferences.readerWindowWidth,
	                  readerWindowHeight: preferences.readerWindowHeight,
	                  readerWindowX: preferences.readerWindowX,
	                  readerWindowY: preferences.readerWindowY,
	                  readerWindowLocked: preferences.readerWindowLocked,
	                  readerWindowPinned: preferences.readerWindowPinned
	                });
	              },
	              createMutationObserver: (callback) => new MutationObserver(callback),
	              ...typeof ResizeObserver == "function" ? {
	                createResizeObserver: (callback) => new ResizeObserver(callback)
	              } : {},
	              requestFrame: (callback) => window.requestAnimationFrame(callback),
	              cancelFrame: (id) => window.cancelAnimationFrame(id)
	            };
	          }
	        },
	        runtime: {
	          document,
	          renderIcon,
	          translationView: bodyTranslationAllowed ? {
	            initialTheme: initialPreferences.translationTheme,
	            subscribeTheme: (listener, themeScope) => {
	              applicationContext.preferenceChanges.subscribe(
	                (preferences) => listener(preferences.translationTheme),
	                themeScope
	              );
	            },
	            ...customSites.translation ? {
	              initialAnimation: customSites.translation.snapshot.config.animation,
	              subscribeAnimation: (listener, animationScope) => {
	                customSites.translation.changes.subscribe(
	                  (snapshot) => listener(snapshot.config.animation),
	                  animationScope
	                );
	              }
	            } : {}
	          } : !1,
	          storage: window.localStorage,
	          sourceId: sourceId(window),
	          locks: window.navigator.locks ?? null,
	          indexedDb: window.indexedDB ?? null,
	          storageEvents: window,
	          broadcastChannelFactory: typeof BroadcastChannel == "function" ? (name) => new BroadcastChannel(name) : null,
	          permit: {
	            shortWindowMs: 1e4,
	            longWindowMs: 6e4,
	            shortBudget: 50,
	            longBudget: 200,
	            minIntervalMs: initialPreferences.performanceRequestInterval,
	            maxConcurrent: initialPreferences.performanceRequestConcurrency,
	            backgroundIdleIntervalMs: import_browser_shared_request_permit.READER_BACKGROUND_REQUEST_IDLE_INTERVAL_MS,
	            backgroundMaxDeferMs: import_browser_shared_request_permit.READER_BACKGROUND_REQUEST_MAX_DEFER_MS
	          },
	          data: {
	            scheduler: {
	              maxConcurrent: initialPreferences.performanceRequestConcurrency,
	              queueLimit: 160,
	              defaultTimeoutMs: 15e3
	            },
	            rateLimit: {
	              evidenceWindowMs: 4e3,
	              maxEndpointEntries: 128,
	              retryAfterFallbackMs: 1500,
	              baseUrl: origin
	            },
	            responseMemoryMaxEntries: 96,
	            responseMemoryMaxBytes: 24 * 1024 * 1024,
	            responsePersistentMaxEntries: 600,
	            responsePersistentMaxBytes: 96 * 1024 * 1024,
	            responseOperationTimeoutMs: 5e3,
	            cacheFlightTtlMs: 3e4,
	            cacheFlightStaleMs: 45e3
	          },
	          topic: {
	            authScope: currentUsername ? `account:${currentUsername}` : `anonymous:${origin}`,
	            origin,
	            pageSize: initialPreferences.performancePageSize,
	            caches: {
	              topic: {
	                freshForMs: 30 * 6e4,
	                retainForMs: 10080 * 6e4,
	                persist: !0
	              },
	              posts: {
	                freshForMs: 30 * 6e4,
	                retainForMs: 10080 * 6e4,
	                persist: !0
	              },
	              nested: {
	                freshForMs: 30 * 6e4,
	                retainForMs: 10080 * 6e4,
	                persist: !0
	              },
	              snapshot: {
	                freshForMs: 30 * 6e4,
	                retainForMs: 720 * 60 * 6e4
	              }
	            }
	          },
	          timelineView: {
	            preferences: {
	              pageStep: initialPreferences.performancePageSize
	            }
	          },
	          history: {
	            panelView: {
	              preferences: {
	                sortMode: initialPreferences.historySortMode
	              },
	              topicHref: (entry) => `${origin}/t/${entry.topicId}`,
	              changeSortMode: (historySortMode) => {
	                applicationContext.updatePreferences?.({
	                  historySortMode
	                });
	              }
	            }
	          },
	          media: {
	            ...katex ? { katex } : {},
	            ...hls ? { hls } : {},
	            hasManagedMediaSource: "ManagedMediaSource" in window
	          },
	          lightbox: {
	            mount: () => template?.view.surfaceHost ?? document.body
	          },
	          topicFactory: {
	            createDomOptions: (bundle, context, _root, services) => {
	              const presentation = (0, import_reader_post_presentation.createReaderPostPresentation)({
	                document,
	                presentation: services.presentation,
	                relativeTime: services.relativeTime,
	                exactTime: services.exactTime,
	                readTopic: () => bundle.services.session.topic,
	                currentUsername: services.currentUsername,
	                recoverAvatarSource: services.recoverAvatarSource,
	                renderIcon
	              }), readState = (0, import_reader_post_presentation.createReaderPostReadStateFeature)({
	                readState: bundle.services.read,
	                parentScope: context.scope,
	                renderIcon,
	                prefersReducedMotion: () => !!window.matchMedia?.(
	                  "(prefers-reduced-motion: reduce)"
	                ).matches,
	                isVisible: (view) => {
	                  const postRoot = view.slots.root;
	                  if (!postRoot.isConnected || !postRoot.getClientRects().length)
	                    return !1;
	                  const rect = postRoot.getBoundingClientRect(), viewport = (postRoot.closest(
	                    ".ldp-descendant-replies-list"
	                  ) ?? template?.view.body)?.getBoundingClientRect() ?? {
	                    top: 0,
	                    right: window.innerWidth,
	                    bottom: window.innerHeight,
	                    left: 0
	                  };
	                  return rect.bottom > viewport.top && rect.top < viewport.bottom && rect.right > viewport.left && rect.left < viewport.right;
	                }
	              }), readViewport = new import_read_viewport_adapter.ReaderPostReadViewportFeature({
	                controller: bundle.services.read,
	                document,
	                parentScope: context.scope,
	                rootFor: (postRoot) => {
	                  const discussion = postRoot.closest(
	                    ".ldp-descendant-replies-list"
	                  );
	                  return discussion || (postRoot.closest(
	                    ".ldp-lb-comment-list,.ldp-topic-action-rail"
	                  ) ? !1 : template?.view.body ?? null);
	                }
	              });
	              return {
	                estimatedRootSize: 360,
	                identity: presentation.identity,
	                render: presentation.render,
	                postFeatures: Object.freeze([readViewport, readState]),
	                replyTreePreferences: {
	                  read: () => replyTreePreferences.read(),
	                  subscribe: (listener, preferenceScope) => replyTreePreferences.subscribe(
	                    listener,
	                    preferenceScope
	                  )
	                }
	              };
	            }
	          },
	          unwantedTopicFilter,
	          onTopicFeatureError: (diagnostic) => {
	            console.error(
	              `[main-lite:${diagnostic.feature}]`,
	              diagnostic.cause
	            );
	          }
	        },
	        translation: {
	          ...customSites.translation ? {
	            readConfig: async () => (await customSites.translation.load()).config
	          } : {},
	          fingerprint: (texts) => {
	            const subtle = window.crypto?.subtle;
	            return subtle ? (0, import_translation_text.translationTextFingerprint)(texts, subtle) : Promise.reject(new Error("浏览器缺少 SubtleCrypto"));
	          },
	          translationCache: {
	            kind: "translations",
	            tags: ["translation:zh-CN"],
	            freshForMs: 720 * 60 * 6e4,
	            retainForMs: 4320 * 60 * 6e4,
	            persist: !0
	          },
	          credentialCache: {
	            kind: "translation-credentials",
	            tags: ["translation:credential"],
	            freshForMs: 8 * 6e4,
	            retainForMs: 8 * 6e4,
	            persist: !1
	          }
	        },
	        resources: {
	          baseUrl: origin,
	          cache: {
	            kind: "images",
	            tags: ["images"],
	            freshForMs: 1440 * 6e4,
	            retainForMs: 720 * 60 * 6e4,
	            persist: !0
	          }
	        },
	        selectPerformancePreferences: (preferences) => preferences,
	        performanceBudgetCeilings: { short: 50, long: 200 },
	        layout: import_reader_layout_style_controller.readerPreferencesLayoutAdapter,
	        appearance: import_reader_appearance_style_controller.readerPreferencesAppearanceAdapter,
	        theme: {
	          preferences: import_reader_theme_controller.readerPreferencesThemeAdapter,
	          hostTheme: (0, import_native_host_api.discourseNativeTheme)(environment.discourseHost),
	          clock: (0, import_reader_local_sun_clock.createReaderBrowserThemeClock)({ window, document }),
	          system: {
	            readDark: () => !!window.matchMedia?.(
	              "(prefers-color-scheme: dark)"
	            ).matches,
	            subscribe: (listener, scope2) => {
	              const query = window.matchMedia?.(
	                "(prefers-color-scheme: dark)"
	              );
	              if (!query) return () => {
	              };
	              const onChange = () => listener(query.matches);
	              query.addEventListener("change", onChange);
	              const cleanup = () => query.removeEventListener("change", onChange);
	              return scope2.add(cleanup), cleanup;
	            }
	          }
	        },
	        font: import_reader_font_style_controller.readerPreferencesFontAdapter,
	        motion: {
	          ...import_reader_motion_settings_form.readerPreferencesMotionAdapter,
	          siteName
	        },
	        image: import_reader_image_preferences.readerPreferencesImageAdapter,
	        boostCopy: import_boost_copy_rule.readerPreferencesBoostCopyAdapter,
	        topicActionRail: import_reader_topic_action_rail.readerPreferencesTopicActionRailAdapter,
	        openQueue: {
	          read: (preferences) => Object.freeze({
	            openTopicsAtFirstPost: preferences.openTopicsAtFirstPost,
	            readerQueueAlwaysVisibleWhenEmpty: preferences.readerQueueAlwaysVisibleWhenEmpty,
	            doubleEscapeToCloseReader: preferences.doubleEscapeToCloseReader,
	            confirmNativeComposerClose: preferences.confirmNativeComposerClose
	          }),
	          createPatch: (preferences) => preferences
	        },
	        shortcuts: import_reader_shortcut_controller.readerPreferencesShortcutAdapter,
	        settings: {
	          view: { brandName: "Awesome LinuxDo Reader" },
	          sitesForm: customSites,
	          ...customSites.webDav ? {
	            webDav: {
	              ...customSites.webDav,
	              customSites: customSites.repository,
	              preferencesCodec
	            }
	          } : {},
	          aboutContent: {
	            version: scriptVersion
	          },
	          configuration: {
	            codec: preferencesCodec,
	            defaults: preferencesDefaults,
	            customSites: customSites.repository,
	            translation: customSites.translation,
	            webDav: customSites.webDav?.repository ?? null
	          },
	          performanceForm: import_reader_performance_settings_form.readerPreferencesPerformanceSettingsAdapter,
	          ...customSites.translation ? {
	            translationForm: {
	              repository: customSites.translation,
	              presentation: {
	                readTheme: () => applicationContext.readPreferences().translationTheme,
	                persistTheme: (translationTheme) => {
	                  if (!applicationContext.updatePreferences)
	                    throw new Error("当前环境不能保存译文样式");
	                  applicationContext.updatePreferences({ translationTheme });
	                }
	              }
	            }
	          } : {},
	          imageForm: import_reader_image_preferences.readerPreferencesImageAdapter,
	          readingForm: import_reader_reading_settings_form.readerPreferencesReadingSettingsAdapter,
	          interactionForm: {
	            boostCopy: import_boost_copy_rule.readerPreferencesBoostCopyAdapter,
	            topicActionRail: import_reader_topic_action_rail.readerPreferencesTopicActionRailAdapter,
	            replyTree: import_reader_reply_tree_preferences.readerPreferencesReplyTreeAdapter,
	            replyTreePreview: replyTreePreferences,
	            boostsAvailable: () => (0, import_native_host_api.discourseNativeBoostsAvailable)(
	              environment.discourseHost
	            )
	          }
	        },
	        selectHistoryNavigationPreferences: (preferences) => ({
	          edgeTriggerPercent: preferences.historyEdgeTriggerPercent,
	          buttonsAlwaysVisible: preferences.historyButtonsAlwaysVisible
	        }),
	        selectHistoryPanelPreferences: (preferences) => ({
	          sortMode: preferences.historySortMode
	        }),
	        selectBookmarkPreferences: (preferences) => ({
	          tabOrder: preferences.bookmarkTabOrder
	        }),
	        selectTimelineViewPreferences: (preferences) => ({
	          pageStep: preferences.performancePageSize
	        }),
	        ...bodyTranslationAllowed ? {
	          selectTranslationMode: (preferences) => preferences.translationMode,
	          persistTranslationMode: (translationMode) => persistTranslationMode?.(translationMode)
	        } : {},
	        targets: {
	          openInitialRoute: !suppressInitialTopicOpen,
	          serviceWorkerMessages,
	          selectOpenTopicsAtFirstPost: (preferences) => preferences.openTopicsAtFirstPost,
	          onError: (error) => {
	            console.error("[main-lite:target]", error);
	          }
	        },
	        onReady(runtime, context, _settings, _settingsView, _layout, appearance, font) {
	          state.runtime = runtime, persistTranslationMode = (translationMode) => {
	            context.updatePreferences?.({ translationMode });
	          };
	          let settingsResetReminderChecked = !1;
	          const checkSettingsResetReminder = () => {
	            settingsResetReminderChecked || !["opening", "running", "failed"].includes(runtime.shell.state) || (settingsResetReminderChecked = !0, (0, import_reader_settings_reset_reminder.showReaderSettingsResetReminder)({
	              storage: window.localStorage,
	              preferencesStorageKey: import_reader_preferences_schema.READER_PREFERENCES_STORAGE_KEY,
	              defaults: preferencesDefaults,
	              update: (preferences) => {
	                if (!context.updatePreferences)
	                  throw new Error("偏好写端口不可用");
	                context.updatePreferences(preferences), (0, import_reader_open_queue_session.requestReaderQueueSurfacePositionsReset)(document);
	              },
	              feedback: runtime.feedback,
	              isActive: () => !runtime.scope.destroyed,
	              onError: (error) => {
	                console.error("[main-lite:settings-reset-reminder]", error);
	              }
	            }));
	          }, restoreOpenedHostTopicTitle = () => {
	            if (runtime.shell.state !== "running") return;
	            const topicId = runtime.shell.activeTopicId;
	            topicId !== null && hostTopicEnhancement?.markTopicOpened(topicId);
	          };
	          runtime.shell.changes.subscribe(
	            () => {
	              checkSettingsResetReminder(), restoreOpenedHostTopicTitle();
	            },
	            runtime.scope
	          ), checkSettingsResetReminder(), restoreOpenedHostTopicTitle();
	          const portal = state.portal;
	          if (!portal) throw new Error("main-lite Shadow Portal 未就绪");
	          const embeddedReload = routeKind === "list" ? new import_reader_embedded_reload_coordinator.ReaderEmbeddedReloadCoordinator({
	            target: window,
	            storage: window.sessionStorage,
	            currentHostRoute: () => `${document.location.pathname}${document.location.search}${document.location.hash}`,
	            navigationType: () => {
	              const entry = window.performance.getEntriesByType?.("navigation")[0];
	              return entry?.type ? entry.type : window.performance.navigation?.type === 1 ? "reload" : null;
	            },
	            capture: () => {
	              const workspace = runtime.workspace.workspace.snapshot, topicId = runtime.shell.activeTopicId, active = runtime.shell.activeValue;
	              if (!workspace.presentation.embedded || topicId === null || !active) return null;
	              const anchor = runtime.historyNavigation.captureCurrent();
	              return anchor ? Object.freeze({
	                mode: workspace.presentation.mode,
	                topicId: Number(topicId),
	                anchor,
	                onlyOp: active.topicOnlyOp.snapshot.enabled
	              }) : null;
	            },
	            restore: async (reload) => {
	              if (!runtime.workspace.setMode(reload.mode)) return !1;
	              const opened = await runtime.openTarget({
	                topicId: reload.topicId,
	                ...reload.anchor.viewport.scrollRatio === void 0 ? { postNumber: reload.anchor.viewport.postNumber } : {},
	                source: "restore",
	                alignment: "nearest"
	              });
	              return opened.topic.status !== "opened" && opened.topic.status !== "reused" ? !1 : (reload.onlyOp && opened.topic.value.topicOnlyOp.setEnabled(!0), await runtime.historyNavigation.restore(
	                reload.topicId,
	                reload.anchor
	              ), !0);
	            },
	            parentScope: runtime.scope,
	            onError: (error) => {
	              console.error("[main-lite:embedded-reload]", error);
	            }
	          }) : null;
	          (async () => {
	            if (await embeddedReload?.restore() || runtime.shell.activeTopicId !== null || !["embed-left", "embed-right"].includes(
	              runtime.workspace.workspace.snapshot.requestedMode
	            ) || !runtime.workspace.workspace.snapshot.canEmbed) return;
	            const recent = runtime.history.ordered("recent-viewed")[0] ?? null;
	            if (!recent) return;
	            const anchor = runtime.historyNavigation.snapshot.states[String(recent.topicId)] ?? (recent.viewport === null ? null : (0, import_reader_history_model.normalizeReaderHistoryAnchorState)({
	              viewport: recent.viewport
	            })), opened = await runtime.openTarget({
	              topicId: recent.topicId,
	              source: "restore",
	              alignment: "nearest"
	            });
	            anchor && (opened.topic.status === "opened" || opened.topic.status === "reused") && await runtime.historyNavigation.restore(recent.topicId, anchor, {
	              highlight: !1,
	              restoreSemanticState: !1
	            });
	          })().catch((error) => {
	            console.error("[main-lite:embedded-default-topic]", error);
	          });
	          const composerWindow = new import_reader_native_composer_window.ReaderNativeComposerWindowController({
	            document,
	            window,
	            mount: portal.root,
	            pageRoot: document.documentElement,
	            readPreferences: context.readPreferences,
	            preferenceChanges: context.preferenceChanges,
	            updatePreferences: (patch) => {
	              context.updatePreferences?.(patch);
	            },
	            readFontProfile: () => font?.snapshot.settings.fontProfile ?? context.readPreferences().fontProfile,
	            ...font ? {
	              fontChanges: {
	                subscribe(listener, scope2) {
	                  return font.changes.subscribe((snapshot) => {
	                    listener(snapshot.settings.fontProfile);
	                  }, scope2);
	                }
	              }
	            } : {},
	            ...appearance ? {
	              readAppearance: () => appearance.snapshot.interaction,
	              appearanceChanges: {
	                subscribe(listener, scope2) {
	                  return appearance.changes.subscribe((snapshot) => {
	                    listener(snapshot.interaction);
	                  }, scope2);
	                }
	              }
	            } : {},
	            createMutationObserver: (callback) => new MutationObserver(callback),
	            requestFrame: (callback) => window.requestAnimationFrame(callback),
	            cancelFrame: (frameId) => window.cancelAnimationFrame(frameId),
	            parentScope: runtime.scope,
	            onError: (error) => {
	              console.error("[main-lite:native-composer-window]", error);
	            }
	          }), unbindComposerWindow = runtime.composer.bindWindow(
	            composerWindow
	          );
	          return runtime.composer.warmReply(), () => {
	            unbindComposerWindow(), composerWindow.destroy(), embeddedReload?.destroy(), persistTranslationMode = null, state.runtime === runtime && (state.runtime = null);
	          };
	        }
	      }).setup(scope, applicationContext);
	    }
	  });
	}
	function startMainLiteUserscript(userscriptGlobal = globalThis) {
	  const environment = new import_browser_userscript_environment.BrowserUserscriptEnvironment({
	    userscriptGlobal
	  }), page = pageRecord(environment.pageWindow), existing = page[DEBUG_HANDLE_KEY] ?? page[LEGACY_DEBUG_HANDLE_KEY], document = page.document, window = environment.pageWindow;
	  if (!document) throw new Error("main-lite document 不可用");
	  if ((0, import_browser_shared_request_permit.isReaderCloudflareChallengeWindow)(window)) {
	    existing?.destroy?.(), page[CHALLENGE_MONITOR_KEY]?.();
	    const stopMonitor = (0, import_browser_shared_request_permit.monitorReaderCloudflareChallengeWindow)({
	      storage: window.localStorage,
	      storageEvents: window,
	      close: () => window.close(),
	      schedule: (callback, intervalMs) => window.setInterval(callback, intervalMs),
	      cancel: (handle2) => window.clearInterval(Number(handle2)),
	      onError: (error) => {
	        console.warn("[main-lite] Cloudflare 验证浮窗自动关闭失败", error);
	      }
	    });
	    return Object.defineProperty(page, CHALLENGE_MONITOR_KEY, {
	      configurable: !0,
	      enumerable: !1,
	      value: stopMonitor,
	      writable: !1
	    }), null;
	  }
	  if (document.location.hostname === "credit.linux.do")
	    return (0, import_reader_credit_account_bridge.scheduleReaderCreditAccountBridge)(
	      window,
	      document,
	      environment.createValueStorage(),
	      environment.createCreditBridgeHttp(),
	      (cause) => console.warn("[main-lite] LDC 账户桥同步失败", cause)
	    ), null;
	  const bypassNativeTab = (0, import_reader_native_topic_route.consumeReaderNativeTabBypass)(window), bypassNativeUrl = (0, import_reader_native_topic_route.consumeReaderNativeBypass)(
	    document.location.href,
	    document.location.origin,
	    (cleanHref) => {
	      try {
	        window.history.replaceState(
	          window.history.state,
	          "",
	          cleanHref
	        );
	      } catch {
	      }
	    }
	  ), suppressInitialTopicOpen = bypassNativeTab || bypassNativeUrl;
	  if (suppressInitialTopicOpen && existing?.destroy?.(), existing && !suppressInitialTopicOpen)
	    return existing;
	  const serviceWorkerMessages = (0, import_reader_userscript_target_adapter.createReaderUserscriptServiceWorkerMessageRelay)(
	    window.navigator.serviceWorker ?? null,
	    (error) => {
	      console.error("[main-lite:service-worker-target]", error);
	    }
	  ), valueStorage = environment.createValueStorage(), customSiteRepository = new import_reader_custom_site_repository.ReaderCustomSiteRepository({
	    storage: valueStorage
	  }), translation = valueStorage ? new import_reader_translation_config.ReaderTranslationConfigRepository({ storage: valueStorage }) : null;
	  let customSiteProbe = null;
	  try {
	    customSiteProbe = environment.createDiscourseSiteProbe();
	  } catch {
	  }
	  let webDav = null;
	  if (valueStorage)
	    try {
	      webDav = Object.freeze({
	        client: environment.createWebDavClient(),
	        repository: new import_reader_webdav_config_repository.ReaderWebDavConfigRepository({
	          storage: valueStorage
	        })
	      });
	    } catch {
	    }
	  const preferences = (0, import_reader_preferences_schema.createReaderPreferencesRepository)({
	    environment: {
	      viewportWidth: window.innerWidth,
	      viewportHeight: window.innerHeight
	    },
	    storage: window.localStorage
	  }), state = {
	    runtime: null,
	    portal: null,
	    diagnostics: []
	  }, onWindowKeyDown = (event) => {
	    state.runtime?.shell.activeValue?.topicContextSurface.handleEscape(event);
	  };
	  window.addEventListener("keydown", onWindowKeyDown, !0);
	  const application = (0, import_reader_userscript_application.createReaderUserscriptApplication)({
	    environment,
	    document,
	    window,
	    preferences,
	    isVerifiedHost: async (hostname, signal) => {
	      if ((0, import_reader_custom_site_repository.readerBuiltinDiscourseHost)(hostname)) return !0;
	      if (signal.aborted) return !1;
	      try {
	        const verified = await customSiteRepository.allows(hostname);
	        return !signal.aborted && verified;
	      } catch {
	        return !1;
	      }
	    },
	    stages: [
	      (0, import_reader_application_stages.createPreferencesStorageSyncStage)({
	        key: import_reader_preferences_schema.READER_PREFERENCES_STORAGE_KEY,
	        window,
	        repository: preferences,
	        onError: (cause) => {
	          console.error("[main-lite:preferences-sync]", cause);
	        }
	      }),
	      createStyleStage(environment, document, state),
	      createRuntimeStage(
	        environment,
	        document,
	        window,
	        state,
	        serviceWorkerMessages,
	        suppressInitialTopicOpen,
	        {
	          repository: customSiteRepository,
	          probe: customSiteProbe,
	          translation,
	          webDav
	        }
	      )
	    ]
	  });
	  application.diagnostics.subscribe((diagnostic) => {
	    state.diagnostics.push(diagnostic), console.error(
	      `[main-lite:${diagnostic.stage}]`,
	      diagnostic.cause
	    );
	  });
	  const started = application.start(), handle = Object.freeze({
	    application,
	    started,
	    get diagnostics() {
	      return Object.freeze([...state.diagnostics]);
	    },
	    get runtime() {
	      return state.runtime;
	    },
	    destroy() {
	      window.removeEventListener("keydown", onWindowKeyDown, !0);
	      try {
	        application.destroy();
	      } finally {
	        serviceWorkerMessages?.destroy(), page[DEBUG_HANDLE_KEY] === handle && delete page[DEBUG_HANDLE_KEY], page[LEGACY_DEBUG_HANDLE_KEY] === handle && delete page[LEGACY_DEBUG_HANDLE_KEY];
	      }
	    }
	  });
	  return Object.defineProperty(page, DEBUG_HANDLE_KEY, {
	    configurable: !0,
	    enumerable: !1,
	    value: handle,
	    writable: !1
	  }), Object.defineProperty(page, LEGACY_DEBUG_HANDLE_KEY, {
	    configurable: !0,
	    enumerable: !1,
	    value: handle,
	    writable: !1
	  }), handle;
	}
	const startMianLiteUserscript = startMainLiteUserscript;
}, "5fc996e88d95c38e19c87ced5961d31fd0576585956abe0474c3d3928129bbfd");

/* Source: lite/src/userscript/main-lite-entry.ts */
runtime.register("src/userscript/main-lite-entry.js", function(module, exports, require) {
	var import_main_lite_bootstrap = require("./main-lite-bootstrap.js");
	(0, import_main_lite_bootstrap.startMainLiteUserscript)();
}, "9e50c00fd0262b242afcb1582b85bc77d9ea61386a6ca6dc6939dc68f814e1ff");

/* Source: lite/src/userscript/reader-embedded-reload-coordinator.ts */
runtime.register("src/userscript/reader-embedded-reload-coordinator.js", function(module, exports, require) {
	var reader_embedded_reload_coordinator_exports = {};
	__export(reader_embedded_reload_coordinator_exports, {
	  ReaderEmbeddedReloadCoordinator: () => ReaderEmbeddedReloadCoordinator
	});
	module.exports = __toCommonJS(reader_embedded_reload_coordinator_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_reader_history_model = require("../history/reader-history-model.js"), import_lifecycle = require("../kernel/lifecycle.js");
	const DEFAULT_STORAGE_KEY = "ldp:mian-lite:embedded-reload:v1", DEFAULT_TTL_MS = 3e4;
	function embeddedMode(value) {
	  return value === "embed-left" || value === "embed-right" ? value : null;
	}
	function stateRecord(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	class ReaderEmbeddedReloadCoordinator {
	  scope;
	  #options;
	  #storageKey;
	  #ttlMs;
	  #now;
	  #restorePromise = null;
	  constructor(options) {
	    this.#options = options, this.#storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY, this.#ttlMs = Math.max(0, options.ttlMs ?? DEFAULT_TTL_MS), this.#now = options.now ?? Date.now, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(options.target, "pagehide", () => {
	      this.save();
	    });
	  }
	  save() {
	    if (this.scope.destroyed) return !1;
	    let capture;
	    try {
	      capture = this.#options.capture();
	    } catch (cause) {
	      return this.#report(cause), !1;
	    }
	    const mode = embeddedMode(capture?.mode);
	    if (!capture || !mode) return !1;
	    const anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(capture.anchor);
	    if (!anchor) return !1;
	    let topicId;
	    try {
	      topicId = Number((0, import_identifiers.discourseTopicId)(capture.topicId));
	    } catch {
	      return !1;
	    }
	    try {
	      const state = Object.freeze({
	        savedAt: this.#now(),
	        hostRoute: String(this.#options.currentHostRoute()),
	        mode,
	        topicId,
	        anchor,
	        onlyOp: capture.onlyOp === !0
	      });
	      return this.#options.storage.setItem(
	        this.#storageKey,
	        JSON.stringify(state)
	      ), !0;
	    } catch (cause) {
	      return this.#report(cause), !1;
	    }
	  }
	  restore() {
	    if (this.#restorePromise) return this.#restorePromise;
	    const transaction = this.#restore();
	    return this.#restorePromise = transaction, transaction.finally(() => {
	      this.#restorePromise === transaction && (this.#restorePromise = null);
	    }), transaction;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #restore() {
	    if (this.scope.destroyed) return !1;
	    const state = this.#consume();
	    if (!state) return !1;
	    try {
	      return await this.#options.restore(state);
	    } catch (cause) {
	      return this.#report(cause), !1;
	    }
	  }
	  #consume() {
	    let raw = null;
	    try {
	      raw = this.#options.storage.getItem(this.#storageKey), this.#options.storage.removeItem(this.#storageKey);
	    } catch (cause) {
	      return this.#report(cause), null;
	    }
	    if (!raw || this.#options.navigationType() !== "reload") return null;
	    let source;
	    try {
	      source = stateRecord(JSON.parse(raw));
	    } catch {
	      return null;
	    }
	    const savedAt = Number(source?.savedAt), mode = embeddedMode(source?.mode), anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(source?.anchor);
	    if (!source || !Number.isFinite(savedAt) || this.#now() - savedAt < 0 || this.#now() - savedAt > this.#ttlMs || String(source.hostRoute ?? "") !== String(this.#options.currentHostRoute()) || !mode || !anchor) return null;
	    let topicId;
	    try {
	      topicId = Number((0, import_identifiers.discourseTopicId)(source.topicId));
	    } catch {
	      return null;
	    }
	    return Object.freeze({
	      savedAt,
	      hostRoute: String(source.hostRoute),
	      mode,
	      topicId,
	      anchor,
	      onlyOp: source.onlyOp === !0
	    });
	  }
	  #report(cause) {
	    try {
	      this.#options.onError?.(cause);
	    } catch {
	    }
	  }
	}
}, "3140edd91b8288950ef4bc855c11df2651f0fccda7a2e4fa0adeef29bbf49d1d");

/* Source: lite/src/userscript/reader-floating-host-target-controller.ts */
runtime.register("src/userscript/reader-floating-host-target-controller.js", function(module, exports, require) {
	var reader_floating_host_target_controller_exports = {};
	__export(reader_floating_host_target_controller_exports, {
	  ReaderFloatingHostTargetController: () => ReaderFloatingHostTargetController
	});
	module.exports = __toCommonJS(reader_floating_host_target_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_workspace = require("../shell/reader-workspace.js"), import_reader_userscript_target_adapter = require("./reader-userscript-target-adapter.js");
	class ReaderFloatingHostTargetController {
	  scope;
	  #options;
	  #requestFrame;
	  #cancelFrame;
	  #target = null;
	  #frame = 0;
	  #clientX = 0;
	  #clientY = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#options = options, this.#requestFrame = options.requestFrame ?? ((callback) => requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((id) => cancelAnimationFrame(id)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(options.overlay, "pointermove", (event) => this.#onPointerMove(event)), this.scope.listen(options.overlay, "click", (event) => this.#onClick(event)), this.scope.add(() => this.#clear());
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  #canSwitch() {
	    return this.#options.workspace.snapshot.presentation.floating && !this.#options.window.snapshot.pinned && this.#options.window.snapshot.viewportWidth > import_reader_workspace.READER_COMPACT_MAX_WIDTH;
	  }
	  #onPointerMove(event) {
	    if (event.target !== this.#options.overlay || event.pointerType === "touch" || !this.#canSwitch()) {
	      this.#stop();
	      return;
	    }
	    this.#clientX = event.clientX, this.#clientY = event.clientY, this.#frame || (this.#frame = this.#requestFrame(() => this.#syncTarget()));
	  }
	  #onClick(event) {
	    if (event.target !== this.#options.overlay || !this.#canSwitch()) return;
	    const target = this.#targetAt(event.clientX, event.clientY);
	    if (this.#stop(), !target) {
	      this.#run(() => this.#options.closeReader());
	      return;
	    }
	    const openAtFirst = this.#readOpenAtFirstPost();
	    this.#run(() => this.#options.target.openTarget({
	      topicId: target.topicId,
	      ...openAtFirst ? { postNumber: (0, import_identifiers.tryDiscoursePostNumber)(1) } : target.postNumber === null ? {} : { postNumber: target.postNumber },
	      source: "link"
	    }));
	  }
	  #syncTarget() {
	    this.#frame = 0, this.#setTarget(
	      this.#canSwitch() ? this.#targetAt(this.#clientX, this.#clientY)?.anchor ?? null : null
	    );
	  }
	  #targetAt(clientX, clientY) {
	    let source = null;
	    try {
	      this.#options.overlay.classList.add("ldp-reader-hit-test-hidden"), source = this.#options.document.elementFromPoint(clientX, clientY);
	    } finally {
	      this.#options.overlay.classList.remove("ldp-reader-hit-test-hidden");
	    }
	    const anchor = source?.closest("a[href]") ?? null;
	    if (!anchor || anchor.classList.contains("ldp-open")) return null;
	    let currentUrl;
	    try {
	      currentUrl = this.#options.currentUrl();
	    } catch (error) {
	      return this.#report(error), null;
	    }
	    const route = (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(
	      anchor.getAttribute("href") ?? anchor.href,
	      currentUrl
	    );
	    return !route || route.bypassReader ? null : Object.freeze({
	      anchor,
	      topicId: route.topicId,
	      postNumber: route.postNumber
	    });
	  }
	  #setTarget(target) {
	    target !== this.#target && (this.#target?.classList.remove("ldp-reader-switch-target"), this.#target = target, this.#target?.classList.add("ldp-reader-switch-target"), this.#options.overlay.classList.toggle(
	      "ldp-reader-switch-ready",
	      this.#target !== null
	    ));
	  }
	  #stop() {
	    this.#frame && this.#cancelFrame(this.#frame), this.#frame = 0, this.#setTarget(null);
	  }
	  #clear() {
	    this.#stop();
	  }
	  #readOpenAtFirstPost() {
	    try {
	      return this.#options.readOpenTopicsAtFirstPost?.() === !0;
	    } catch (error) {
	      return this.#report(error), !1;
	    }
	  }
	  #run(action) {
	    try {
	      Promise.resolve(action()).catch((error) => this.#report(error));
	    } catch (error) {
	      this.#report(error);
	    }
	  }
	  #report(error) {
	    try {
	      this.#options.onError?.(error);
	    } catch {
	    }
	  }
	}
}, "3ed6bdf9714346b7cbaa0b69040534a7b798c0def2a9ec57ccf47683f55144ec");

/* Source: lite/src/userscript/reader-host-topic-preheat-controller.ts */
runtime.register("src/userscript/reader-host-topic-preheat-controller.js", function(module, exports, require) {
	var reader_host_topic_preheat_controller_exports = {};
	__export(reader_host_topic_preheat_controller_exports, {
	  ReaderHostTopicPreheatController: () => ReaderHostTopicPreheatController
	});
	module.exports = __toCommonJS(reader_host_topic_preheat_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_userscript_target_adapter = require("./reader-userscript-target-adapter.js");
	const CARD_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item", TOPIC_LINK_SELECTOR = 'a.raw-topic-link[href*="/t/"],a.title[href*="/t/"],a[href*="/t/"]', META_SELECTOR = ".ldp-host-topic-reader-meta", PERFORMANCE_CARD_CLASS = "ldp-host-topic-card-performance", DEFAULT_MAX_QUEUED_TOPICS = 6, DEFAULT_MAX_CONCURRENT_PREHEATS = 1, MAX_CONCURRENT_PREHEATS = 3, HOST_REPLY_COUNT_SELECTORS = Object.freeze([
	  ".ldp-topic-stat--reply .ldp-topic-stat-value",
	  ":scope > td.posts .number",
	  ":scope > td.posts",
	  ".topic-stats .posts .number",
	  ".topic-stats .posts",
	  ".topic-list-data.posts .number",
	  ".topic-list-data.posts"
	]);
	function compactCount(value) {
	  const match = String(value ?? "").replaceAll(",", "").replaceAll(",", "").trim().match(/(\d+(?:\.\d+)?)\s*(k|m|万|亿)?/i);
	  if (!match) return null;
	  const unit = String(match[2] ?? "").toLocaleLowerCase("en-US"), multiplier = unit === "k" ? 1e3 : unit === "m" ? 1e6 : unit === "万" ? 1e4 : unit === "亿" ? 1e8 : 1, count = Math.floor(Number(match[1]) * multiplier);
	  return Number.isSafeInteger(count) && count >= 0 ? count : null;
	}
	function hostCardPostCount(card) {
	  for (const selector of HOST_REPLY_COUNT_SELECTORS) {
	    const source = card.querySelector(selector);
	    if (source)
	      for (const value of [
	        source.dataset.count,
	        source.dataset.value,
	        source.getAttribute("title"),
	        source.getAttribute("aria-label"),
	        source.textContent
	      ]) {
	        const replies = compactCount(value);
	        if (replies !== null) return replies + 1;
	      }
	  }
	  return 0;
	}
	function elementFromNode(node) {
	  return node ? node.nodeType === 1 ? node : node.parentElement : null;
	}
	function historyPostNumber(entry) {
	  if (!entry) return null;
	  try {
	    return (0, import_identifiers.discoursePostNumber)(entry.viewport?.postNumber ?? entry.postNumber);
	  } catch {
	    return entry.postNumber;
	  }
	}
	function historyDate(timestamp) {
	  const date = new Date(timestamp);
	  return Number.isFinite(date.getTime()) ? new Intl.DateTimeFormat("zh-CN", {
	    month: "numeric",
	    day: "numeric",
	    hour: "2-digit",
	    minute: "2-digit",
	    hour12: !1
	  }).format(date) : "未知时间";
	}
	class ReaderHostTopicPreheatController {
	  scope;
	  #options;
	  #document;
	  #maxQueuedTopics;
	  #maxConcurrentPreheats;
	  #requestFrame;
	  #cancelFrame;
	  #cards = /* @__PURE__ */ new Map();
	  #topics = /* @__PURE__ */ new Map();
	  #pendingCards = /* @__PURE__ */ new Set();
	  #nearTopics = /* @__PURE__ */ new Map();
	  #liveReading = /* @__PURE__ */ new Map();
	  #queue = [];
	  #activeControllers = /* @__PURE__ */ new Map();
	  #networkActiveTopics = /* @__PURE__ */ new Set();
	  #observer;
	  #resumePromise = null;
	  #paused = !1;
	  #frame = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#options = options, this.#document = options.document, this.#maxQueuedTopics = Math.max(
	      1,
	      Math.floor(options.maxQueuedTopics ?? DEFAULT_MAX_QUEUED_TOPICS)
	    ), this.#maxConcurrentPreheats = Math.min(
	      MAX_CONCURRENT_PREHEATS,
	      Math.max(
	        1,
	        Math.floor(
	          options.maxConcurrentPreheats ?? DEFAULT_MAX_CONCURRENT_PREHEATS
	        )
	      )
	    );
	    const view = this.#document.defaultView;
	    this.#requestFrame = options.requestFrame ?? ((callback) => {
	      const request = view?.requestAnimationFrame;
	      return typeof request == "function" ? request.call(view, callback) : (callback(0), 0);
	    }), this.#cancelFrame = options.cancelFrame ?? ((id) => {
	      const cancel = view?.cancelAnimationFrame;
	      typeof cancel == "function" && cancel.call(view, id);
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const NativeIntersectionObserver = this.#document.defaultView?.IntersectionObserver, createObserver = options.createIntersectionObserver ?? (NativeIntersectionObserver ? ((callback, init) => new NativeIntersectionObserver(callback, init)) : null);
	    this.#observer = createObserver?.(
	      (entries) => this.#onIntersections(entries),
	      { root: null, rootMargin: "125% 0px", threshold: 0.01 }
	    ) ?? null, options.mutations.subscribe((batch) => this.#onMutations(batch), this.scope), this.scope.add(() => this.#clear());
	  }
	  destroy() {
	    this.#destroyed || (this.#destroyed = !0, this.scope.destroy());
	  }
	  refreshHistory() {
	    if (!(this.#destroyed || this.scope.destroyed))
	      for (const [card, cardState] of this.#cards) {
	        const topic = this.#topics.get(cardState.topicId);
	        if (!topic) continue;
	        const totalIncreased = this.#refreshTotalCount(card, topic), target = this.#targetPostNumber(
	          cardState.topicId,
	          cardState.routePostNumber
	        );
	        topic.status !== "loading" && (topic.targetPostNumber = target), this.#renderCard(card, topic), totalIncreased && cardState.near && this.#enqueue(topic);
	      }
	  }
	  refreshConfirmedReadCount(topicId) {
	    if (!(this.#destroyed || this.scope.destroyed))
	      for (const topic of this.#topics.values())
	        topicId !== void 0 && topic.topicId !== topicId || (topic.confirmedReadCount = this.#confirmedReadCount(topic.topicId), this.#renderTopic(topic));
	  }
	  updateLiveReading(topicId, postNumber, confirmedReadCount, viewedAt = Date.now()) {
	    if (this.#destroyed || this.scope.destroyed) return;
	    const topic = this.#topics.get(topicId), timestamp = Number(viewedAt);
	    this.#liveReading.set(topicId, Object.freeze({
	      postNumber,
	      viewedAt: Number.isFinite(timestamp) && timestamp > 0 ? timestamp : Date.now()
	    })), topic && (this.#stopPreheatForLiveTopic(topic), topic.confirmedReadCount = Math.max(
	      0,
	      Math.floor(Number(confirmedReadCount) || 0)
	    ), this.#renderTopic(topic));
	  }
	  clearLiveReading(topicId) {
	    if (this.#destroyed || this.scope.destroyed) return;
	    const topicIds = topicId === void 0 ? [...this.#liveReading.keys()] : [topicId];
	    for (const currentTopicId of topicIds) {
	      if (!this.#liveReading.delete(currentTopicId)) continue;
	      const topic = this.#topics.get(currentTopicId);
	      topic && (topic.confirmedReadCount = this.#confirmedReadCount(currentTopicId), this.#renderTopic(topic), this.#nearTopics.has(currentTopicId) && this.#enqueue(topic));
	    }
	  }
	  #onMutations(batch) {
	    if (!this.#destroyed) {
	      if (batch.rootChanged) {
	        for (const card of [...this.#cards.keys()])
	          batch.root?.contains(card) || this.#releaseCard(card);
	        batch.root && this.#collectCards(batch.root);
	      }
	      for (const record of batch.records)
	        if (!elementFromNode(record.target)?.closest(META_SELECTOR)) {
	          record.type === "characterData" && this.#collectCards(record.target);
	          for (const node of record.removedNodes) this.#releaseCardsIn(node);
	          for (const node of record.addedNodes) this.#collectCards(node);
	        }
	      this.#scheduleCards();
	    }
	  }
	  #collectCards(node) {
	    const element = elementFromNode(node);
	    if (!element || element.closest(".ldp-overlay,.ldp-reader-portal-host") || element.matches(META_SELECTOR)) return;
	    const nearest = element.matches(CARD_SELECTOR) ? element : element.closest(CARD_SELECTOR);
	    if (nearest) {
	      this.#pendingCards.add(nearest);
	      return;
	    }
	    for (const card of element.querySelectorAll(CARD_SELECTOR))
	      this.#pendingCards.add(card);
	  }
	  #releaseCardsIn(node) {
	    const element = elementFromNode(node);
	    if (element) {
	      element.matches(CARD_SELECTOR) && this.#releaseCard(element);
	      for (const card of element.querySelectorAll(CARD_SELECTOR))
	        this.#releaseCard(card);
	    }
	  }
	  #scheduleCards() {
	    this.#frame || !this.#pendingCards.size || (this.#frame = this.#requestFrame(() => {
	      this.#frame = 0;
	      const cards = [...this.#pendingCards];
	      this.#pendingCards.clear();
	      for (const card of cards)
	        card.isConnected && this.#attachCard(card);
	    }));
	  }
	  #attachCard(card) {
	    const current = this.#cards.get(card);
	    if (current) {
	      const topic2 = this.#topics.get(current.topicId);
	      if (topic2) {
	        const totalIncreased = this.#refreshTotalCount(card, topic2);
	        this.#renderCard(card, topic2), totalIncreased && current.near && this.#enqueue(topic2);
	      }
	      return;
	    }
	    const href = card.querySelector(TOPIC_LINK_SELECTOR)?.getAttribute("href") ?? "", route = href ? (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(href, this.#document.baseURI) : null;
	    let topicId;
	    try {
	      topicId = route?.topicId ?? (0, import_identifiers.discourseTopicId)(
	        card.dataset.topicId ?? card.getAttribute("data-topic-id")
	      );
	    } catch {
	      return;
	    }
	    const meta = this.#document.createElement("span");
	    meta.className = META_SELECTOR.slice(1), card.classList.add(PERFORMANCE_CARD_CLASS), (card.querySelector(".link-bottom-line") ?? card.querySelector(".main-link") ?? card).append(meta);
	    const cardState = {
	      topicId,
	      routePostNumber: route?.postNumber ?? null,
	      meta,
	      near: !1
	    };
	    this.#cards.set(card, cardState);
	    let topic = this.#topics.get(topicId);
	    topic || (topic = {
	      topicId,
	      targetPostNumber: this.#targetPostNumber(
	        topicId,
	        cardState.routePostNumber
	      ),
	      status: "idle",
	      warmedCount: 0,
	      totalCount: Math.max(
	        this.#options.historyEntry(topicId)?.postsCount ?? 0,
	        hostCardPostCount(card)
	      ),
	      cacheHit: !1,
	      confirmedReadCount: this.#confirmedReadCount(topicId),
	      attempts: 0,
	      restoreAttempted: !1,
	      restorePending: !1,
	      restoreController: null,
	      cards: /* @__PURE__ */ new Set()
	    }, this.#topics.set(topicId, topic)), topic.cards.add(card), this.#renderCard(card, topic), this.#observer ? this.#observer.observe(card) : this.#enqueue(topic);
	  }
	  #releaseCard(card) {
	    this.#pendingCards.delete(card);
	    const current = this.#cards.get(card);
	    if (!current) return;
	    this.#observer?.unobserve(card), current.meta.remove(), card.classList.remove(PERFORMANCE_CARD_CLASS), this.#cards.delete(card);
	    const topic = this.#topics.get(current.topicId);
	    topic?.cards.delete(card), topic && ![...topic.cards].some(
	      (candidate) => this.#cards.get(candidate)?.near === !0
	    ) && this.#nearTopics.delete(topic.topicId), topic && !topic.cards.size && (topic.status === "queued" && (this.#removeFromQueue(topic.topicId), topic.status = "idle"), topic.restoreController?.abort(
	      new DOMException("Topic 已离开宿主预热区", "AbortError")
	    ), topic.restoreController = null, topic.restorePending = !1, this.#activeControllers.get(topic.topicId)?.abort(
	      new DOMException("Topic 已离开宿主预热区", "AbortError")
	    ), topic.status !== "loading" && this.#topics.delete(topic.topicId));
	  }
	  #targetPostNumber(topicId, routePostNumber) {
	    const historical = historyPostNumber(this.#options.historyEntry(topicId));
	    if (historical !== null) return historical;
	    try {
	      if (this.#options.readOpenTopicsAtFirstPost()) return (0, import_identifiers.discoursePostNumber)(1);
	    } catch (error) {
	      this.#report(error);
	    }
	    return routePostNumber ?? (0, import_identifiers.discoursePostNumber)(1);
	  }
	  #onIntersections(entries) {
	    for (const entry of entries) {
	      const card = entry.target, cardState = this.#cards.get(card), topic = cardState ? this.#topics.get(cardState.topicId) : null;
	      if (!cardState || !topic) continue;
	      if (cardState.near = entry.isIntersecting, !entry.isIntersecting) {
	        [...topic.cards].some(
	          (candidate) => this.#cards.get(candidate)?.near === !0
	        ) || this.#nearTopics.delete(topic.topicId);
	        continue;
	      }
	      const rect = entry.boundingClientRect, viewportCenter = entry.rootBounds ? (entry.rootBounds.top + entry.rootBounds.bottom) / 2 : Number(this.#document.defaultView?.innerHeight ?? 0) / 2, cardCenter = Number.isFinite(rect?.top) && Number.isFinite(rect?.bottom) ? (rect.top + rect.bottom) / 2 : viewportCenter;
	      this.#nearTopics.set(topic.topicId, Math.abs(cardCenter - viewportCenter));
	    }
	    if (this.#dropStaleQueuedTopics(), this.#nearTopics.size)
	      for (const [topicId, controller] of this.#activeControllers)
	        this.#nearTopics.has(topicId) || controller.abort(
	          new DOMException("Topic 已离开宿主预热区", "AbortError")
	        );
	    if (this.#paused) {
	      this.#tryResume();
	      return;
	    }
	    this.#fillQueue();
	  }
	  #fillQueue() {
	    if (!this.#paused) {
	      this.#dropStaleQueuedTopics();
	      for (const [topicId] of [...this.#nearTopics.entries()].sort((left, right) => left[1] - right[1])) {
	        if (this.#queue.length >= this.#maxQueuedTopics) break;
	        const topic = this.#topics.get(topicId);
	        topic && this.#enqueue(topic);
	      }
	    }
	  }
	  #dropStaleQueuedTopics() {
	    const retained = this.#queue.filter((topicId) => {
	      const topic = this.#topics.get(topicId);
	      return !topic || topic.status !== "queued" ? !1 : this.#nearTopics.has(topicId) ? !0 : (topic.status = "idle", this.#renderTopic(topic), !1);
	    });
	    retained.sort((left, right) => (this.#nearTopics.get(left) ?? Number.POSITIVE_INFINITY) - (this.#nearTopics.get(right) ?? Number.POSITIVE_INFINITY)), this.#queue.splice(0, this.#queue.length, ...retained);
	  }
	  #enqueue(topic) {
	    this.#liveReading.has(topic.topicId) || topic.status === "queued" || topic.status === "loading" || topic.status === "partial" || topic.status === "ready" || topic.attempts >= 2 || this.#queue.length >= this.#maxQueuedTopics || (topic.status = "queued", this.#queue.push(topic.topicId), this.#renderTopic(topic), this.#restorePreheat(topic), this.#pump());
	  }
	  #pump() {
	    if (!(this.#destroyed || this.scope.destroyed || this.#paused)) {
	      for (let index = this.#queue.length - 1; index >= 0; index -= 1) {
	        const topicId = this.#queue[index], topic = this.#topics.get(topicId);
	        topic?.status === "queued" && !this.#liveReading.has(topicId) || (this.#queue.splice(index, 1), topic?.status === "queued" && (topic.status = "idle", this.#renderTopic(topic)));
	      }
	      for (; this.#networkActiveTopics.size < this.#maxConcurrentPreheats && this.#activeControllers.size < this.#maxQueuedTopics + this.#maxConcurrentPreheats; ) {
	        const queueIndex = this.#queue.findIndex((topicId2) => this.#topics.get(topicId2)?.restorePending !== !0);
	        if (queueIndex < 0) return;
	        const [topicId] = this.#queue.splice(queueIndex, 1);
	        if (topicId === void 0) return;
	        const topic = this.#topics.get(topicId);
	        if (!topic || topic.status !== "queued") continue;
	        const controller = new AbortController();
	        this.#activeControllers.set(topicId, controller), this.#networkActiveTopics.add(topicId), topic.status = "loading", topic.attempts += 1, this.#renderTopic(topic);
	        let task;
	        try {
	          task = this.#options.preheat(
	            topic.topicId,
	            topic.targetPostNumber,
	            controller.signal,
	            (progress) => {
	              this.#applyProgress(topic, progress, !1), progress.complete && !controller.signal.aborted && this.#releaseNetworkSlot(topicId, controller);
	            },
	            topic.totalCount
	          );
	        } catch (error) {
	          task = Promise.reject(error);
	        }
	        task.then((result) => {
	          controller.signal.aborted || this.#applyProgress(topic, result, !0);
	        }).catch((error) => {
	          if (controller.signal.aborted) {
	            topic.status = "idle", topic.attempts = Math.max(0, topic.attempts - 1), this.#renderTopic(topic);
	            return;
	          }
	          topic.status = "error", this.#renderTopic(topic), this.#shouldPause(error) && this.#pauseQueue(), this.#report(error), topic.cards.size || this.#topics.delete(topic.topicId);
	        }).finally(() => {
	          this.#networkActiveTopics.delete(topicId), this.#activeControllers.get(topicId) === controller && (this.#activeControllers.delete(topicId), this.#paused || (this.#fillQueue(), this.#pump()));
	        });
	      }
	    }
	  }
	  #restorePreheat(topic) {
	    if (topic.restoreAttempted || !this.#options.restorePreheat) return;
	    topic.restoreAttempted = !0, topic.restorePending = !0;
	    const controller = new AbortController();
	    topic.restoreController = controller, this.#options.restorePreheat(
	      topic.topicId,
	      topic.targetPostNumber,
	      controller.signal
	    ).then((progress) => {
	      if (progress === null || controller.signal.aborted || this.#destroyed || this.scope.destroyed || this.#topics.get(topic.topicId) !== topic) return;
	      const minimumTotalCount = topic.totalCount;
	      topic.warmedCount = Math.max(0, Math.floor(progress.warmedCount)), topic.totalCount = Math.max(
	        minimumTotalCount,
	        Math.max(0, Math.floor(progress.totalCount))
	      ), topic.cacheHit = progress.cacheHit, progress.complete && progress.totalCount >= minimumTotalCount && (topic.status === "queued" || topic.status === "idle") && (this.#removeFromQueue(topic.topicId), topic.status = "ready"), this.#renderTopic(topic);
	    }).catch((error) => {
	      controller.signal.aborted || this.#report(error);
	    }).finally(() => {
	      topic.restoreController === controller && (topic.restoreController = null, topic.restorePending = !1, this.#paused || (this.#fillQueue(), this.#pump()));
	    });
	  }
	  #releaseNetworkSlot(topicId, controller) {
	    this.#activeControllers.get(topicId) !== controller || !this.#networkActiveTopics.delete(topicId) || this.#paused || (this.#fillQueue(), this.#pump());
	  }
	  #removeFromQueue(topicId) {
	    for (let index = this.#queue.length - 1; index >= 0; index -= 1)
	      this.#queue[index] === topicId && this.#queue.splice(index, 1);
	  }
	  #shouldPause(error) {
	    try {
	      return this.#options.shouldPauseAfterError?.(error) === !0;
	    } catch (cause) {
	      return this.#report(cause), !0;
	    }
	  }
	  #pauseQueue() {
	    this.#paused = !0;
	    for (const topicId of this.#queue.splice(0)) {
	      const queued = this.#topics.get(topicId);
	      queued?.status === "queued" && (queued.status = "idle", this.#renderTopic(queued));
	    }
	    for (const controller of this.#activeControllers.values())
	      controller.abort(
	        new DOMException("宿主 Topic 预热因统一限流暂停", "AbortError")
	      );
	  }
	  #stopPreheatForLiveTopic(topic) {
	    this.#removeFromQueue(topic.topicId), topic.restoreController?.abort(
	      new DOMException("Topic 已进入 Reader,停止宿主预热", "AbortError")
	    ), this.#activeControllers.get(topic.topicId)?.abort(
	      new DOMException("Topic 已进入 Reader,停止宿主预热", "AbortError")
	    ), topic.status !== "ready" && (topic.status = "idle"), this.#renderTopic(topic), this.#pump();
	  }
	  #tryResume() {
	    if (this.#resumePromise) return this.#resumePromise;
	    const promise = Promise.resolve(this.#options.canResume?.() ?? !0).then((ready) => {
	      !ready || this.#destroyed || this.scope.destroyed || (this.#paused = !1, this.#fillQueue(), this.#pump());
	    }).catch((error) => this.#report(error)).finally(() => {
	      this.#resumePromise === promise && (this.#resumePromise = null);
	    });
	    return this.#resumePromise = promise, promise;
	  }
	  #applyProgress(topic, progress, settled) {
	    const minimumTotalCount = topic.totalCount;
	    topic.warmedCount = Math.max(0, Math.floor(progress.warmedCount)), topic.totalCount = Math.max(
	      minimumTotalCount,
	      Math.max(0, Math.floor(progress.totalCount))
	    ), topic.cacheHit = progress.cacheHit, progress.complete && progress.totalCount >= minimumTotalCount ? topic.status = "ready" : settled && (topic.status = "partial"), this.#renderTopic(topic), settled && !topic.cards.size && this.#topics.delete(topic.topicId);
	  }
	  #refreshTotalCount(card, topic) {
	    const totalCount = Math.max(
	      this.#options.historyEntry(topic.topicId)?.postsCount ?? 0,
	      hostCardPostCount(card)
	    );
	    return totalCount <= topic.totalCount ? !1 : (topic.totalCount = totalCount, (topic.status === "ready" || topic.status === "partial" || topic.status === "error") && (topic.status = "idle", topic.attempts = 0), !0);
	  }
	  #renderTopic(topic) {
	    for (const card of [...topic.cards])
	      card.isConnected ? this.#renderCard(card, topic) : this.#releaseCard(card);
	  }
	  #renderCard(card, topic) {
	    const cardState = this.#cards.get(card);
	    if (!cardState) return;
	    const history = this.#options.historyEntry(topic.topicId), live = this.#liveReading.get(topic.topicId), floor = live?.postNumber ?? historyPostNumber(history) ?? topic.targetPostNumber, viewedAt = live?.viewedAt ?? history?.viewedAt ?? 0, historyLabel = live || history ? `上次阅读 ${historyDate(viewedAt)} · 定位 #${floor}` : "", total = topic.totalCount || history?.postsCount || 0, suffix = topic.status === "queued" ? "(排队)" : topic.status === "loading" ? "(后台)" : topic.status === "error" ? "(失败)" : topic.status === "partial" ? "(部分)" : "", preheatLabel = `预热 ${topic.warmedCount}/${total || "?"}${suffix}`, activityLabel = live ? "阅读中" : preheatLabel, readLabel = `已读 ${topic.confirmedReadCount}`;
	    cardState.meta.textContent = historyLabel ? `${historyLabel} · ${activityLabel} · ${readLabel}` : `${activityLabel} · ${readLabel}`, cardState.meta.dataset.ldpPreheatState = live ? "reading" : topic.status, cardState.meta.title = live || history ? `上次阅读:${new Date(viewedAt).toLocaleString("zh-CN")};定位:#${floor};${activityLabel};${readLabel}` : `${activityLabel};${readLabel}`;
	  }
	  #confirmedReadCount(topicId) {
	    try {
	      const value = Number(this.#options.readConfirmedCount?.(topicId) ?? 0);
	      return Number.isSafeInteger(value) && value > 0 ? value : 0;
	    } catch (error) {
	      return this.#report(error), 0;
	    }
	  }
	  #clear() {
	    this.#frame && this.#cancelFrame(this.#frame), this.#frame = 0, this.#pendingCards.clear();
	    for (const controller of this.#activeControllers.values())
	      controller.abort(
	        new DOMException("宿主 Topic 预热已释放", "AbortError")
	      );
	    this.#activeControllers.clear(), this.#networkActiveTopics.clear(), this.#resumePromise = null, this.#paused = !1, this.#queue.length = 0, this.#nearTopics.clear(), this.#liveReading.clear();
	    for (const topic of this.#topics.values())
	      topic.restoreController?.abort(
	        new DOMException("宿主 Topic 预热已释放", "AbortError")
	      );
	    this.#observer?.disconnect(), this.#observer = null;
	    for (const [card, current] of this.#cards)
	      current.meta.remove(), card.classList.remove(PERFORMANCE_CARD_CLASS);
	    this.#cards.clear(), this.#topics.clear();
	  }
	  #report(error) {
	    try {
	      this.#options.onError?.(error);
	    } catch {
	    }
	  }
	}
}, "65fe2e21b73182e81a14961453555da1a6b7beb1b0e8d815df4bd5ff5562ddb5");

/* Source: lite/src/userscript/reader-host-topic-source-coordinator.ts */
runtime.register("src/userscript/reader-host-topic-source-coordinator.js", function(module, exports, require) {
	var reader_host_topic_source_coordinator_exports = {};
	__export(reader_host_topic_source_coordinator_exports, {
	  ReaderHostTopicSourceCoordinator: () => ReaderHostTopicSourceCoordinator
	});
	module.exports = __toCommonJS(reader_host_topic_source_coordinator_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_lifecycle = require("../kernel/lifecycle.js");
	const TOPIC_ROW_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item", SOURCE_SELECTOR = ".fk-d-menu,.menu-panel,.search-menu,.user-menu,.hamburger-panel,.sidebar-hamburger-dropdown,.chat-drawer", SURFACE_SELECTOR = ".fk-d-menu,.menu-panel,.chat-drawer", OWNED_SELECTOR = '.ldp-overlay,.ldp-reader-portal-host,[data-ldp-owned="true"]';
	function nativeNotificationTarget(target) {
	  if (target.request.source !== "notification" && target.request.source !== "message") return !1;
	  const menu = target.anchor.closest(".user-menu[data-tab-id]");
	  return !!(menu && !menu.closest(OWNED_SELECTOR));
	}
	function acknowledgeNativeNotification(anchor) {
	  const view = anchor.ownerDocument.defaultView, EventConstructor = view?.MouseEvent;
	  let event;
	  typeof EventConstructor == "function" ? event = new EventConstructor("click", {
	    bubbles: !0,
	    button: 0,
	    cancelable: !0,
	    ctrlKey: !0
	  }) : (event = new (view?.Event ?? Event)("click", {
	    bubbles: !0,
	    cancelable: !0
	  }), Object.defineProperties(event, {
	    button: { configurable: !0, value: 0 },
	    ctrlKey: { configurable: !0, value: !0 }
	  })), event.preventDefault();
	  try {
	    return anchor.dispatchEvent(event), !0;
	  } catch {
	    return !1;
	  }
	}
	function topicIdFromCard(card) {
	  const direct = String(
	    card.getAttribute("data-topic-id") ?? card.dataset.topicId ?? ""
	  ).trim();
	  return direct || ((card.querySelector(
	    'a.raw-topic-link[href*="/t/"],a.title[href*="/t/"],.link-top-line a[href*="/t/"],a[href*="/t/"]'
	  )?.getAttribute("href") ?? "").match(/\/t\/(?:[^/]+\/)?(\d+)(?:\/|$)/)?.[1] ?? "");
	}
	function sourceElement(target) {
	  const anchorSource = target.anchor.closest(SOURCE_SELECTOR), markerSource = target.sourceElement?.closest(
	    SOURCE_SELECTOR
	  ) ?? null;
	  return anchorSource ?? markerSource;
	}
	function sourceSurface(source) {
	  return !source || source.closest(OWNED_SELECTOR) ? null : source.matches(SURFACE_SELECTOR) ? source : source.closest(SURFACE_SELECTOR) ?? source.querySelector(SURFACE_SELECTOR) ?? source;
	}
	function surfaceClosed(surface) {
	  if (!surface.isConnected || surface.hidden === !0 || surface.getAttribute("aria-hidden") === "true") return !0;
	  const view = surface.ownerDocument.defaultView;
	  if (typeof view?.getComputedStyle != "function") return !1;
	  try {
	    const style = view.getComputedStyle(surface);
	    return style.display === "none" || style.visibility === "hidden";
	  } catch {
	    return !1;
	  }
	}
	function openSourceSurfaces(document) {
	  const seen = /* @__PURE__ */ new Set(), result = [];
	  for (const source of document.querySelectorAll(SOURCE_SELECTOR)) {
	    const surface = sourceSurface(source);
	    !surface || seen.has(surface) || surfaceClosed(surface) || (seen.add(surface), result.push(Object.freeze({ source, surface })));
	  }
	  return Object.freeze(result);
	}
	class ReaderHostTopicSourceCoordinator {
	  scope;
	  #document;
	  #host;
	  #anchors = /* @__PURE__ */ new WeakMap();
	  #restoringTargets = /* @__PURE__ */ new Set();
	  #nativeNotificationTargets = /* @__PURE__ */ new Set();
	  #closingOpenSurfaces = null;
	  constructor(options) {
	    this.#document = options.document, this.#host = options.host, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const readerRoot = options.readerRoot, isEmbedded = options.isEmbedded;
	    readerRoot && isEmbedded && this.scope.listen(readerRoot, "pointerdown", () => {
	      this.scope.destroyed || !isEmbedded() || this.closeOpenSurfaces();
	    }, !0), this.scope.add(() => {
	      this.#restoringTargets.clear(), this.#nativeNotificationTargets.clear(), this.#closingOpenSurfaces = null, this.#document.documentElement.classList.remove(
	        "ldp-reader-host-anchor-restoring"
	      );
	    });
	  }
	  async prepare(target) {
	    if (this.scope.destroyed) return;
	    const pointerAnchor = this.#capturePointerAnchor(target);
	    if (pointerAnchor) {
	      this.#anchors.set(target, pointerAnchor), this.#restoringTargets.add(target), this.#document.documentElement.classList.add(
	        "ldp-reader-host-anchor-restoring"
	      );
	      const blur = target.anchor.blur;
	      typeof blur == "function" && blur.call(target.anchor);
	    }
	    if (nativeNotificationTarget(target)) {
	      this.#nativeNotificationTargets.add(target);
	      return;
	    }
	    await this.#closeSourceSurface(target);
	  }
	  closeOpenSurfaces() {
	    if (this.scope.destroyed) return Promise.resolve();
	    if (this.#closingOpenSurfaces) return this.#closingOpenSurfaces;
	    const closing = this.#closeOpenSourceSurfaces().catch(() => {
	    });
	    return this.#closingOpenSurfaces = closing, closing.then(() => {
	      this.#closingOpenSurfaces === closing && (this.#closingOpenSurfaces = null);
	    }), closing;
	  }
	  async settle(target, opened) {
	    this.#nativeNotificationTargets.delete(target) && (opened && !this.scope.destroyed && acknowledgeNativeNotification(target.anchor), (opened || this.#nativeNotificationTargets.size === 0) && await this.#closeSourceSurface(target));
	    const anchor = this.#anchors.get(target) ?? null;
	    this.#anchors.delete(target);
	    let restored = !1;
	    return opened && anchor && !this.scope.destroyed && (await this.#nextFrame(), this.scope.destroyed || (restored = this.#restorePointerAnchor(anchor))), await this.#finishAnchorRestoration(target), restored;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #capturePointerAnchor(target) {
	    const clientY = target.pointer?.clientY;
	    if (target.pointer?.detail === 0 || !Number.isFinite(clientY))
	      return null;
	    const card = target.anchor.closest(TOPIC_ROW_SELECTOR);
	    if (!card || card.closest(OWNED_SELECTOR)) return null;
	    const rect = card.getBoundingClientRect();
	    return !(rect.width > 0) || !(rect.height > 0) ? null : Object.freeze({
	      card,
	      topicId: String(target.request.topicId),
	      clientY,
	      offsetY: Math.max(0, Math.min(rect.height, clientY - rect.top))
	    });
	  }
	  #restorePointerAnchor(anchor) {
	    let card = anchor.card.isConnected ? anchor.card : null;
	    if (card || (card = [...this.#document.querySelectorAll(
	      TOPIC_ROW_SELECTOR
	    )].find(
	      (candidate) => !candidate.closest(OWNED_SELECTOR) && topicIdFromCard(candidate) === anchor.topicId
	    ) ?? null), !card) return !1;
	    for (const candidate of this.#document.querySelectorAll(
	      '[data-ldp-reader-active-topic="true"]'
	    ))
	      candidate !== card && candidate.removeAttribute("data-ldp-reader-active-topic");
	    card.dataset.ldpReaderActiveTopic = "true";
	    const delta = card.getBoundingClientRect().top + anchor.offsetY - anchor.clientY, view = this.#document.defaultView;
	    return Math.abs(delta) >= 0.5 && typeof view?.scrollBy == "function" && view.scrollBy({ top: delta, behavior: "auto" }), !0;
	  }
	  async #finishAnchorRestoration(target) {
	    this.#restoringTargets.delete(target), !(this.#restoringTargets.size || this.scope.destroyed) && (await this.#nextFrame(), !(this.#restoringTargets.size || this.scope.destroyed) && this.#document.documentElement.classList.remove(
	      "ldp-reader-host-anchor-restoring"
	    ));
	  }
	  async #closeSourceSurface(target) {
	    const source = sourceElement(target), surface = sourceSurface(source);
	    !surface || surfaceClosed(surface) || await this.#closeSurface(surface, source);
	  }
	  async #closeOpenSourceSurfaces() {
	    for (const { source, surface } of openSourceSurfaces(this.#document)) {
	      if (this.scope.destroyed) return;
	      await this.#closeSurface(surface, source);
	    }
	  }
	  async #closeSurface(surface, source) {
	    const identifier = surface.matches(".fk-d-menu") ? String(surface.dataset.identifier ?? "").trim() : "";
	    if (identifier) {
	      const close = (0, import_native_host_api.discourseNativeMenuCloser)(this.#host);
	      if (close) {
	        await this.#invokeAndWait(
	          surface,
	          () => close(identifier)
	        );
	        return;
	      }
	    }
	    const chatClose = surface.matches(".chat-drawer") ? surface.querySelector(
	      ".chat-drawer-header__close-btn"
	    ) : null;
	    if (chatClose && typeof chatClose.click == "function") {
	      await this.#invokeAndWait(surface, () => chatClose.click());
	      return;
	    }
	    const controlledIds = [surface.id].filter(Boolean);
	    let trigger = [...this.#document.querySelectorAll(
	      '[aria-expanded="true"]'
	    )].find(
	      (candidate) => controlledIds.some(
	        (id) => [
	          candidate.getAttribute("aria-controls"),
	          candidate.getAttribute("aria-owns")
	        ].some((value) => String(value ?? "").split(/\s+/).includes(id))
	      )
	    ) ?? null;
	    if (!trigger) {
	      const selector = source?.classList.contains("search-menu") ? ".d-header-icons .search-dropdown" : source?.classList.contains("user-menu") ? ".d-header-icons .current-user" : source ? ".d-header-icons .hamburger-dropdown,.hamburger-dropdown" : "";
	      if (selector) {
	        const candidates = [
	          ...this.#document.querySelectorAll(selector)
	        ];
	        trigger = candidates.find(
	          (candidate) => candidate.getAttribute("aria-expanded") === "true"
	        ) ?? candidates[0] ?? null;
	      }
	    }
	    trigger && typeof trigger.click == "function" && await this.#invokeAndWait(surface, () => trigger.click());
	  }
	  async #invokeAndWait(surface, action) {
	    try {
	      await Promise.resolve(action());
	    } catch {
	      return;
	    }
	    surfaceClosed(surface) || this.scope.destroyed || await new Promise((resolve) => {
	      const view = this.#document.defaultView;
	      let settled = !1;
	      const finish = () => {
	        settled || (settled = !0, resolve());
	      }, deadline = Date.now() + 180, check = () => {
	        if (this.scope.destroyed || surfaceClosed(surface) || Date.now() >= deadline) {
	          finish();
	          return;
	        }
	        typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(check) : view?.setTimeout(check, 16);
	      };
	      check();
	    });
	  }
	  async #nextFrame() {
	    const view = this.#document.defaultView;
	    typeof view?.requestAnimationFrame == "function" && await new Promise((resolve) => {
	      view.requestAnimationFrame(() => resolve());
	    });
	  }
	}
}, "7f80358bbea58fd56863d806855971a64c9bcc0adddfe7b6c1ecc50db6d0c73e");

/* Source: lite/src/userscript/reader-userscript-application.ts */
runtime.register("src/userscript/reader-userscript-application.js", function(module, exports, require) {
	var reader_userscript_application_exports = {};
	__export(reader_userscript_application_exports, {
	  createReaderUserscriptApplication: () => createReaderUserscriptApplication,
	  createReaderUserscriptRouteChangePort: () => createReaderUserscriptRouteChangePort,
	  createReaderUserscriptRuntimeBindings: () => createReaderUserscriptRuntimeBindings,
	  createReaderUserscriptRuntimeStage: () => createReaderUserscriptRuntimeStage
	});
	module.exports = __toCommonJS(reader_userscript_application_exports);
	var import_reader_application = require("../app/reader-application.js"), import_reader_browser_runtime = require("../app/reader-browser-runtime.js"), import_native_host_api = require("../discourse/native-host-api.js"), import_value_record = require("../kernel/value-record.js"), import_reader_userscript_target_adapter = require("./reader-userscript-target-adapter.js"), import_reader_host_topic_source_coordinator = require("./reader-host-topic-source-coordinator.js"), import_reader_host_topic_preheat_controller = require("./reader-host-topic-preheat-controller.js"), import_reader_floating_host_target_controller = require("./reader-floating-host-target-controller.js");
	function createReaderUserscriptRouteChangePort(host) {
	  return Object.freeze({
	    subscribe(handler) {
	      if (typeof handler != "function")
	        throw new TypeError("Discourse page-change handler 必须是函数");
	      return (0, import_native_host_api.discourseDeferredSubscription)(() => {
	        const module2 = (0, import_value_record.valueRecord)(
	          host.lookupModule("discourse/lib/plugin-api")
	        ), defaultExport = (0, import_value_record.valueRecord)(module2?.default), owner = typeof module2?.withPluginApi == "function" ? module2 : defaultExport, withPluginApi = owner?.withPluginApi;
	        if (typeof withPluginApi != "function") return null;
	        let bindingActive = !0, pageCleanup = null, pluginCleanup = null;
	        try {
	          const result = withPluginApi.call(
	            owner,
	            (apiValue) => {
	              if (!bindingActive) return;
	              const api = (0, import_value_record.valueRecord)(apiValue), onPageChange = api?.onPageChange;
	              if (typeof onPageChange != "function") return;
	              const cleanup = onPageChange.call(api, () => {
	                bindingActive && handler();
	              });
	              typeof cleanup == "function" && (pageCleanup = cleanup);
	            }
	          );
	          typeof result == "function" && (pluginCleanup = result);
	        } catch {
	          return () => {
	          };
	        }
	        return () => {
	          if (bindingActive) {
	            bindingActive = !1;
	            try {
	              pageCleanup?.();
	            } finally {
	              pluginCleanup?.();
	            }
	          }
	        };
	      });
	    }
	  });
	}
	function createReaderUserscriptApplication(options) {
	  const detectedHost = new import_reader_application.BrowserDiscourseHostPort({
	    moduleLookup: (name) => options.environment.discourseHost.lookupModule(name),
	    document: options.document,
	    window: options.window,
	    ...options.hostTimeoutMs === void 0 ? {} : { timeoutMs: options.hostTimeoutMs },
	    ...options.createHostObserver === void 0 ? {} : { createObserver: options.createHostObserver }
	  }), host = options.isVerifiedHost ? Object.freeze({
	    async waitForHost(signal) {
	      const detected = await detectedHost.waitForHost(signal);
	      if (detected || signal.aborted) return detected;
	      const hostname = String(
	        options.document.location?.hostname ?? options.window.location?.hostname ?? ""
	      ).trim();
	      return !await options.isVerifiedHost(
	        hostname,
	        signal
	      ) || signal.aborted ? null : Object.freeze({ detection: "verified-site" });
	    }
	  }) : detectedHost;
	  return new import_reader_application.ReaderApplication({
	    bodyReady: (signal) => (0, import_reader_application.browserBodyReady)(options.document, signal),
	    host,
	    preferences: options.preferences,
	    stages: options.stages
	  });
	}
	function createReaderUserscriptRuntimeBindings(environment, translationOptions, resourceOptions) {
	  let externalHttp;
	  try {
	    externalHttp = environment.createExternalHttp(
	      translationOptions?.http
	    );
	  } catch (cause) {
	    if (translationOptions) throw cause;
	  }
	  let translation;
	  if (translationOptions) {
	    if (!externalHttp) throw new Error("外部翻译 HTTP capability 不可用");
	    const { http: _httpOptions, ...adapterOptions } = translationOptions;
	    translation = Object.freeze({ ...adapterOptions, http: externalHttp });
	  }
	  let resources;
	  if (resourceOptions) {
	    const http = environment.createPublicResourceHttp();
	    resources = Object.freeze({
	      ...resourceOptions,
	      http,
	      objectUrls: environment.createObjectUrlPort()
	    });
	  }
	  const valueStorage = environment.createValueStorage(), assetCacheStorage = environment.createAssetCacheStorage();
	  return Object.freeze({
	    host: environment.discourseHost,
	    share: environment.createShareSurface(),
	    ...externalHttp ? {
	      connect: Object.freeze({ http: externalHttp }),
	      credit: Object.freeze({
	        http: externalHttp,
	        ...valueStorage ? { storage: valueStorage } : {}
	      })
	    } : {},
	    ...translation === void 0 ? {} : { translation },
	    ...resources === void 0 ? {} : { resources },
	    ...assetCacheStorage ? { assetCacheStorage } : {}
	  });
	}
	function createReaderUserscriptRuntimeStage(options) {
	  const bindings = createReaderUserscriptRuntimeBindings(
	    options.environment,
	    options.translation,
	    options.resources
	  ), valueStorage = options.environment.createValueStorage(), onReady = options.onReady, targetOptions = options.targets === !1 ? null : options.targets ?? Object.freeze({});
	  return (0, import_reader_browser_runtime.createReaderBrowserRuntimeStage)({
	    shell: options.shell,
	    runtime: {
	      ...options.runtime,
	      searchForms: options.runtime.searchForms ?? options.environment.createPinyinSearchForms(),
	      host: bindings.host,
	      share: bindings.share,
	      ...bindings.connect ? { connect: bindings.connect } : {},
	      ...bindings.credit ? { credit: bindings.credit } : {},
	      ...bindings.translation === void 0 ? {} : { translation: bindings.translation },
	      ...bindings.resources === void 0 ? {} : { resources: bindings.resources },
	      ...bindings.assetCacheStorage === void 0 ? {} : { assetCacheStorage: bindings.assetCacheStorage },
	      ...options.runtime.threadContextStorage !== void 0 || !valueStorage ? {} : { threadContextStorage: valueStorage }
	    },
	    ...options.selectNavigationPreferences === void 0 ? {} : {
	      selectNavigationPreferences: options.selectNavigationPreferences
	    },
	    ...options.selectPerformancePreferences === void 0 ? {} : {
	      selectPerformancePreferences: options.selectPerformancePreferences
	    },
	    ...options.performanceBudgetCeilings === void 0 ? {} : {
	      performanceBudgetCeilings: options.performanceBudgetCeilings
	    },
	    ...options.layout === void 0 ? {} : { layout: options.layout },
	    ...options.appearance === void 0 ? {} : { appearance: options.appearance },
	    ...options.theme === void 0 ? {} : { theme: options.theme },
	    ...options.font === void 0 ? {} : { font: options.font },
	    ...options.motion === void 0 ? {} : { motion: options.motion },
	    ...options.image === void 0 ? {} : { image: options.image },
	    ...options.boostCopy === void 0 ? {} : { boostCopy: options.boostCopy },
	    ...options.topicActionRail === void 0 ? {} : { topicActionRail: options.topicActionRail },
	    ...options.openQueue === void 0 ? {} : { openQueue: options.openQueue },
	    ...options.shortcuts === void 0 ? {} : { shortcuts: options.shortcuts },
	    ...options.settings === void 0 ? {} : { settings: options.settings },
	    ...options.selectHistoryNavigationPreferences === void 0 ? {} : {
	      selectHistoryNavigationPreferences: options.selectHistoryNavigationPreferences
	    },
	    ...options.selectHistoryPanelPreferences === void 0 ? {} : {
	      selectHistoryPanelPreferences: options.selectHistoryPanelPreferences
	    },
	    ...options.selectBookmarkPreferences === void 0 ? {} : {
	      selectBookmarkPreferences: options.selectBookmarkPreferences
	    },
	    ...options.selectTimelineViewPreferences === void 0 ? {} : {
	      selectTimelineViewPreferences: options.selectTimelineViewPreferences
	    },
	    ...options.selectTranslationMode === void 0 ? {} : {
	      selectTranslationMode: options.selectTranslationMode
	    },
	    ...options.persistTranslationMode === void 0 ? {} : {
	      persistTranslationMode: options.persistTranslationMode
	    },
	    onReady(runtime, context, settings, settingsView, layout, appearance, font) {
	      let targetAdapter = null, userObservationEntry = null, floatingHostTarget = null, hostSource = null, hostPreheat = null, readyCleanup;
	      try {
	        if (targetOptions) {
	          const routeChanges = createReaderUserscriptRouteChangePort(
	            options.environment.discourseHost
	          );
	          floatingHostTarget = new import_reader_floating_host_target_controller.ReaderFloatingHostTargetController({
	            document: options.runtime.document,
	            overlay: runtime.shell.view.root,
	            workspace: runtime.workspace.workspace,
	            window: runtime.workspace.window,
	            currentUrl: () => options.runtime.document.location.href,
	            target: {
	              openTarget: (request) => runtime.openTarget(request)
	            },
	            closeReader: () => runtime.close(),
	            readOpenTopicsAtFirstPost: () => targetOptions.selectOpenTopicsAtFirstPost?.(
	              context.readPreferences()
	            ) === !0,
	            parentScope: runtime.scope,
	            ...targetOptions.onError === void 0 ? {} : { onError: targetOptions.onError }
	          }), hostSource = new import_reader_host_topic_source_coordinator.ReaderHostTopicSourceCoordinator({
	            document: options.runtime.document,
	            host: options.environment.discourseHost,
	            readerRoot: runtime.shell.view.root,
	            isEmbedded: () => runtime.workspace.workspace.snapshot.presentation.embedded,
	            parentScope: runtime.scope
	          });
	          const readAuthScope = options.runtime.topic.authScope, confirmedReadPosts = /* @__PURE__ */ new Map(), rememberConfirmedReadPosts = (topicId, postNumbers) => {
	            let posts = confirmedReadPosts.get(topicId);
	            posts || (posts = /* @__PURE__ */ new Set(), confirmedReadPosts.set(topicId, posts));
	            for (const postNumber of postNumbers) posts.add(postNumber);
	          };
	          for (const confirmed of runtime.data.readCoordination.confirmedPosts(
	            readAuthScope
	          ))
	            rememberConfirmedReadPosts(
	              confirmed.topicId,
	              [confirmed.postNumber]
	            );
	          hostPreheat = new import_reader_host_topic_preheat_controller.ReaderHostTopicPreheatController({
	            document: options.runtime.document,
	            mutations: runtime.workspace.mutations,
	            maxConcurrentPreheats: 3,
	            historyEntry: (topicId) => runtime.history.entry(topicId),
	            readConfirmedCount: (topicId) => confirmedReadPosts.get(topicId)?.size ?? 0,
	            readOpenTopicsAtFirstPost: () => targetOptions.selectOpenTopicsAtFirstPost?.(
	              context.readPreferences()
	            ) === !0,
	            restorePreheat: async (topicId, postNumber, signal) => {
	              const active = runtime.shell.activeValue;
	              if (active?.services.session.topicId === topicId)
	                return signal.throwIfAborted(), active.services.session.applyPageSize(
	                  runtime.performance.pageSize
	                ), active.services.session.restorePreheatEntry(
	                  postNumber
	                );
	              const scope = runtime.scope.child(), abort = scope.abortController(
	                new DOMException("宿主 Topic 缓存恢复已释放", "AbortError"),
	                signal
	              ), bundle = runtime.data.createTopicBundle({
	                topicId,
	                scope,
	                signal: abort.signal,
	                mount: () => () => {
	                }
	              }, {
	                ...options.runtime.topic,
	                pageSize: runtime.performance.pageSize,
	                refreshCachedInBackground: !1,
	                host: options.environment.discourseHost,
	                nativeAjax: runtime.nativeAjax
	              });
	              try {
	                return await bundle.services.session.restorePreheatEntry(
	                  postNumber
	                );
	              } finally {
	                await bundle.prepareClose?.("close"), scope.destroy();
	              }
	            },
	            preheat: async (topicId, postNumber, signal, report, minimumTotalCount) => {
	              const active = runtime.shell.activeValue;
	              if (active?.services.session.topicId === topicId) {
	                signal.throwIfAborted(), active.services.session.applyPageSize(
	                  runtime.performance.pageSize
	                );
	                const result = await active.services.session.preheatEntry(
	                  postNumber,
	                  {
	                    background: !0,
	                    prefetchTier: "nearby",
	                    maxAttempts: 1,
	                    minimumTotalCount,
	                    onProgress: report,
	                    beforeNetwork: (requestSignal) => {
	                      signal.throwIfAborted(), requestSignal.throwIfAborted();
	                    }
	                  }
	                );
	                return signal.throwIfAborted(), await active.services.session.flush(), result;
	              }
	              const scope = runtime.scope.child(), abort = scope.abortController(
	                new DOMException("宿主 Topic 预热已释放", "AbortError"),
	                signal
	              ), bundle = runtime.data.createTopicBundle({
	                topicId,
	                scope,
	                signal: abort.signal,
	                mount: () => () => {
	                }
	              }, {
	                ...options.runtime.topic,
	                pageSize: runtime.performance.pageSize,
	                refreshCachedInBackground: !1,
	                host: options.environment.discourseHost,
	                nativeAjax: runtime.nativeAjax
	              });
	              try {
	                await bundle.services.session.init({
	                  background: !0,
	                  prefetchTier: "nearby"
	                });
	                const restoredMinimumTotalCount = bundle.services.session.initializedFromCache ? minimumTotalCount : 0, result = await bundle.services.session.preheatEntry(
	                  postNumber,
	                  {
	                    background: !0,
	                    prefetchTier: "nearby",
	                    maxAttempts: 1,
	                    minimumTotalCount: restoredMinimumTotalCount,
	                    onProgress: report
	                  }
	                );
	                return await bundle.services.session.flush(), result;
	              } finally {
	                await bundle.prepareClose?.("close"), scope.destroy();
	              }
	            },
	            shouldPauseAfterError: (error) => runtime.data.client.requestResume(error) !== null,
	            canResume: async () => {
	              const snapshot = await runtime.permit.snapshot();
	              return snapshot.challengeState === "idle" && snapshot.nextPermitDelay <= 0;
	            },
	            parentScope: runtime.scope,
	            ...targetOptions.onError === void 0 ? {} : { onError: targetOptions.onError }
	          }), runtime.history.changes.subscribe(
	            () => hostPreheat?.refreshHistory(),
	            hostPreheat.scope
	          ), hostPreheat.scope.add(
	            runtime.data.readCoordination.subscribeConfirmations(
	              (confirmation) => {
	                confirmation.authScope === readAuthScope && (rememberConfirmedReadPosts(
	                  confirmation.topicId,
	                  confirmation.postNumbers
	                ), hostPreheat?.refreshConfirmedReadCount(
	                  confirmation.topicId
	                ));
	              }
	            )
	          );
	          let releaseActiveReadingProjection = () => {
	          };
	          const clearActiveReadingProjection = () => {
	            releaseActiveReadingProjection(), releaseActiveReadingProjection = () => {
	            }, hostPreheat?.clearLiveReading();
	          }, bindActiveReadingProjection = () => {
	            clearActiveReadingProjection();
	            const active = runtime.shell.activeValue;
	            if (!active) return;
	            const topicId = active.services.session.topicId, sync = (postNumber = active.topicTimeline.snapshot.currentPostNumber) => {
	              rememberConfirmedReadPosts(
	                topicId,
	                active.services.read.snapshot().confirmed
	              ), hostPreheat?.updateLiveReading(
	                topicId,
	                postNumber,
	                confirmedReadPosts.get(topicId)?.size ?? 0
	              );
	            }, releaseTimeline = active.topicTimeline.changes.subscribe(
	              (snapshot) => sync(snapshot.currentPostNumber)
	            ), releaseRead = active.services.read.changes.subscribe(
	              (change) => {
	                change.kind === "confirmed" && (rememberConfirmedReadPosts(topicId, change.postNumbers), sync());
	              }
	            );
	            releaseActiveReadingProjection = () => {
	              releaseTimeline(), releaseRead();
	            }, sync();
	          };
	          runtime.shell.changes.subscribe((state) => {
	            state === "running" ? bindActiveReadingProjection() : (state === "switching" || state === "closed" || state === "failed") && clearActiveReadingProjection();
	          }, hostPreheat.scope), hostPreheat.scope.add(clearActiveReadingProjection), runtime.shell.state === "running" && bindActiveReadingProjection(), targetAdapter = new import_reader_userscript_target_adapter.ReaderUserscriptTargetAdapter({
	            document: options.runtime.document,
	            currentUrl: () => options.runtime.document.location.href,
	            target: {
	              openTarget: (request) => runtime.openTarget(request),
	              openHistoricalTarget: async (request) => {
	                const entry = runtime.history.entry(request.topicId), anchor = entry?.viewport ? { viewport: entry.viewport } : runtime.historyNavigation.snapshot.states[String(request.topicId)] ?? null;
	                if (!anchor) return runtime.openTarget(request);
	                const exactFloorAnchor = Object.freeze({
	                  viewport: Object.freeze({
	                    postNumber: anchor.viewport.postNumber,
	                    postOffset: anchor.viewport.postOffset,
	                    scrollTop: anchor.viewport.scrollTop
	                  }),
	                  replyWindow: null,
	                  quoteHighlight: null
	                }), opened = await runtime.openTarget({
	                  topicId: request.topicId,
	                  source: "restore"
	                });
	                if (opened.topic.status !== "opened" && opened.topic.status !== "reused") return opened;
	                await runtime.historyNavigation.restore(
	                  request.topicId,
	                  exactFloorAnchor,
	                  {
	                    highlight: !1,
	                    restoreSemanticState: !0
	                  }
	                );
	                const active = runtime.shell.activeValue;
	                active?.services.session.topicId === request.topicId && (await active.services.session.flush(), active.dom.flushNow());
	                const navigation = active?.services.session.topicId === request.topicId ? await active.topicNavigation.navigate({
	                  postNumber: exactFloorAnchor.viewport.postNumber,
	                  source: "history",
	                  alignment: "center",
	                  highlight: !1
	                }) : null;
	                if (navigation?.status === "revealed" && active) {
	                  active.dom.flushNow();
	                  const releaseTimelineHold = active.topicTimeline.holdVisiblePost(
	                    exactFloorAnchor.viewport.postNumber
	                  ), settleTimers = /* @__PURE__ */ new Set(), clearSettleTimers = () => {
	                    for (const timer of settleTimers)
	                      options.runtime.document.defaultView?.clearTimeout(timer);
	                    settleTimers.clear();
	                  };
	                  let releaseUserIntent = () => {
	                  }, releaseReaderInteraction = () => {
	                  };
	                  const releaseHistorySettle = () => {
	                    clearSettleTimers(), releaseTimelineHold(), releaseUserIntent(), releaseReaderInteraction();
	                  };
	                  releaseUserIntent = active.dom.listenDirectUserScrollIntent(
	                    releaseHistorySettle
	                  ), releaseReaderInteraction = active.topicTimeline.scope.listen(
	                    runtime.shell.view.root,
	                    "click",
	                    releaseHistorySettle
	                  ), active.topicTimeline.scope.add(clearSettleTimers);
	                  for (const delayMs of [200, 800, 2e3]) {
	                    const timerWindow = options.runtime.document.defaultView;
	                    if (!timerWindow) break;
	                    const timer = timerWindow.setTimeout(() => {
	                      settleTimers.delete(timer), !(runtime.shell.activeValue !== active || active.services.session.topicId !== request.topicId) && active.topicNavigation.navigate({
	                        postNumber: exactFloorAnchor.viewport.postNumber,
	                        source: "history",
	                        alignment: "center",
	                        highlight: !1,
	                        cachedOnly: !0
	                      }).then((settled) => {
	                        settled.status === "revealed" && runtime.shell.activeValue === active && active.dom.flushNow();
	                      }).catch(() => {
	                      });
	                    }, delayMs);
	                    settleTimers.add(timer);
	                  }
	                }
	                return Object.freeze({
	                  topic: opened.topic,
	                  navigation: Object.freeze({
	                    status: navigation?.status ?? "superseded"
	                  })
	                });
	              }
	            },
	            routeChanges,
	            serviceWorkerMessages: targetOptions.serviceWorkerMessages === void 0 ? options.runtime.document.defaultView?.navigator.serviceWorker ?? null : targetOptions.serviceWorkerMessages,
	            readHistoryPostNumber: (topicId) => {
	              const history = runtime.history.entry(topicId);
	              return history?.viewport?.postNumber ?? history?.postNumber ?? null;
	            },
	            readOpenTopicsAtFirstPost: () => targetOptions.selectOpenTopicsAtFirstPost?.(
	              context.readPreferences()
	            ) === !0,
	            ...targetOptions.openInitialRoute === void 0 ? {} : {
	              openInitialRoute: targetOptions.openInitialRoute
	            },
	            ...targetOptions.interceptTopicLinks === void 0 ? {} : {
	              interceptTopicLinks: targetOptions.interceptTopicLinks
	            },
	            beforeOpenTarget: async (target) => {
	              await hostSource.prepare(target), await targetOptions.beforeOpenTarget?.(target);
	            },
	            afterOpenTarget: async (target, opened) => {
	              await hostSource.settle(target, opened);
	            },
	            parentScope: runtime.scope,
	            ...targetOptions.onError === void 0 ? {} : { onError: targetOptions.onError }
	          }), userObservationEntry = new import_reader_userscript_target_adapter.ReaderUserscriptUserObservationEntry({
	            document: options.runtime.document,
	            currentUrl: () => options.runtime.document.location.href,
	            routeChanges,
	            hostMutations: {
	              subscribe: (handler) => runtime.workspace.mutations.subscribe(handler)
	            },
	            openObservation: (identity) => {
	              runtime.userObservationView.observeAndOpen(identity);
	            },
	            parentScope: runtime.scope,
	            ...targetOptions.onError === void 0 ? {} : { onError: targetOptions.onError }
	          });
	        }
	        readyCleanup = onReady?.(
	          runtime,
	          context,
	          settings,
	          settingsView,
	          layout,
	          appearance,
	          font
	        ) || void 0;
	      } catch (error) {
	        throw userObservationEntry?.destroy(), targetAdapter?.destroy(), floatingHostTarget?.destroy(), hostSource?.destroy(), hostPreheat?.destroy(), error;
	      }
	      return () => {
	        try {
	          readyCleanup?.();
	        } finally {
	          userObservationEntry?.destroy(), targetAdapter?.destroy(), floatingHostTarget?.destroy(), hostSource?.destroy(), hostPreheat?.destroy();
	        }
	      };
	    }
	  });
	}
}, "23ce69b7741716b95a17d29b0919a57bd789d784b7ba51aeb1e10beea3e63ab4");

/* Source: lite/src/userscript/reader-userscript-target-adapter.ts */
runtime.register("src/userscript/reader-userscript-target-adapter.js", function(module, exports, require) {
	var reader_userscript_target_adapter_exports = {};
	__export(reader_userscript_target_adapter_exports, {
	  ReaderUserscriptTargetAdapter: () => ReaderUserscriptTargetAdapter,
	  ReaderUserscriptUserObservationEntry: () => ReaderUserscriptUserObservationEntry,
	  createReaderUserscriptServiceWorkerMessageRelay: () => createReaderUserscriptServiceWorkerMessageRelay,
	  parseReaderUserscriptTopicRoute: () => parseReaderUserscriptTopicRoute,
	  parseReaderUserscriptUserRoute: () => parseReaderUserscriptUserRoute,
	  readerUserscriptRouteKind: () => readerUserscriptRouteKind
	});
	module.exports = __toCommonJS(reader_userscript_target_adapter_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_native_topic_route = require("../topic/reader-native-topic-route.js");
	function createReaderUserscriptServiceWorkerMessageRelay(source, onError) {
	  if (!source) return null;
	  const listeners = /* @__PURE__ */ new Set();
	  let destroyed = !1;
	  const forward = (event) => {
	    for (const listener of [...listeners])
	      try {
	        listener.call(source, event);
	      } catch (error) {
	        try {
	          onError?.(error);
	        } catch {
	        }
	      }
	  };
	  try {
	    source.addEventListener("message", forward, !0);
	  } catch (error) {
	    try {
	      onError?.(error);
	    } catch {
	    }
	    return null;
	  }
	  return Object.freeze({
	    addEventListener(_type, listener) {
	      destroyed || listeners.add(listener);
	    },
	    removeEventListener(_type, listener) {
	      listeners.delete(listener);
	    },
	    destroy() {
	      destroyed || (destroyed = !0, listeners.clear(), source.removeEventListener("message", forward, !0));
	    }
	  });
	}
	const SOURCE_SELECTOR = "[data-reader-target-source],.ldp-notification-item", BYPASS_SELECTOR = '[data-reader-target-interception="off"],[data-reader-target-source="history"],.ldp-open,.ldp-history-link', HOST_TOPIC_CARD_SELECTOR = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item", HOST_TOPIC_LINK_SELECTOR = 'a.raw-topic-link[href*="/t/"],a.title[href*="/t/"],.link-top-line a[href*="/t/"],a[href*="/t/"]', HOST_TOPIC_CARD_CONTROL_SELECTOR = 'a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[contenteditable="true"],[data-user-card],[data-ldp-native-dnd]', READER_OWNED_SELECTOR = '.ldp-overlay,.ldp-reader-portal-host,[data-ldp-owned="true"]', NATIVE_NOTIFICATION_TAB_IDS = /* @__PURE__ */ new Set([
	  "all-notifications",
	  "replies",
	  "likes",
	  "messages",
	  "other-notifications"
	]), HOST_USER_OBSERVATION_ENTRY_SELECTOR = ".ldp-host-user-observation-entry", HOST_USER_PROFILE_NAME_SELECTORS = Object.freeze([
	  ".user-main .user-profile-names > .user-profile-names__primary",
	  ".user-main .user-profile-names > .full-name",
	  ".user-main .primary-textual > .full-name",
	  ".user-main .primary-textual > h1",
	  ".user-main .user-profile-names > .username",
	  ".user-main .primary-textual > .username"
	]), HOST_USER_PROFILE_USERNAME_SELECTORS = Object.freeze([
	  ".user-main .user-profile-names__secondary.username",
	  ".user-main .user-profile-names > .username:not(.user-profile-names__primary)",
	  ".user-main .primary-textual > .username"
	]), HOST_USER_PROFILE_RETRY_DELAYS = Object.freeze([
	  50,
	  100,
	  250,
	  500,
	  1e3,
	  2e3
	]);
	function element(value) {
	  return value === null || typeof value != "object" || value.nodeType !== 1 || typeof value.matches != "function" ? null : value;
	}
	function eventAnchor(event) {
	  const path = typeof event.composedPath == "function" ? event.composedPath() : [];
	  for (const value of path) {
	    const candidate = element(value);
	    if (candidate?.matches("a[href]")) return candidate;
	  }
	  return element(event.target)?.closest("a[href]") ?? null;
	}
	function eventHostTopicCardAnchor(event) {
	  const target = element(event.target), card = target?.closest(HOST_TOPIC_CARD_SELECTOR) ?? null;
	  return !target || !card || card.closest(READER_OWNED_SELECTOR) || target.closest(HOST_TOPIC_CARD_CONTROL_SELECTOR) ? null : card.querySelector(HOST_TOPIC_LINK_SELECTOR);
	}
	function nativeHostNotificationSource(anchor) {
	  const menu = anchor.closest(".user-menu[data-tab-id]");
	  if (!menu || menu.closest(".ldp-overlay,.ldp-reader-portal-host")) return null;
	  const tabId = String(menu.dataset.tabId ?? "").trim();
	  return NATIVE_NOTIFICATION_TAB_IDS.has(tabId) ? tabId === "messages" ? "message" : "notification" : null;
	}
	function linkSource(marker) {
	  const explicit = marker?.getAttribute("data-reader-target-source");
	  return explicit === "link" || explicit === "message" || explicit === "notification" || explicit === "restore" ? explicit : marker?.classList.contains("ldp-notification-message-item") || marker?.getAttribute("data-notification-mode") === "messages" ? "message" : marker?.classList.contains("ldp-notification-item") ? "notification" : "link";
	}
	function markerRoute(marker, fallback) {
	  if (!marker) return fallback;
	  const topicId = (0, import_identifiers.tryDiscourseTopicId)(
	    marker.getAttribute("data-reader-topic-id") ?? marker.getAttribute("data-notification-topic-id")
	  );
	  if (!topicId) return fallback;
	  const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(
	    marker.getAttribute("data-reader-post-number") ?? marker.getAttribute("data-notification-post-number")
	  ) ?? (fallback?.topicId === topicId ? fallback.postNumber : null);
	  return Object.freeze({
	    topicId,
	    postNumber,
	    bypassReader: fallback?.bypassReader ?? !1,
	    href: fallback?.href ?? ""
	  });
	}
	function isPlainPrimaryClick(event) {
	  const pointer = event;
	  return event.type === "click" && !event.defaultPrevented && (pointer.button === void 0 || pointer.button === 0) && pointer.altKey !== !0 && pointer.ctrlKey !== !0 && pointer.metaKey !== !0 && pointer.shiftKey !== !0;
	}
	function truthyAttribute(node, name) {
	  const value = node?.getAttribute(name);
	  return value === "1" || value === "true";
	}
	function isSameOriginHttpTarget(value, baseValue) {
	  try {
	    const base = new URL(baseValue), url = new URL(value, base);
	    return /^https?:$/i.test(url.protocol) && url.origin === base.origin;
	  } catch {
	    return !1;
	  }
	}
	function parseReaderUserscriptTopicRoute(value, baseValue) {
	  let base, url;
	  try {
	    base = new URL(baseValue), url = new URL(value, base);
	  } catch {
	    return null;
	  }
	  if (!/^https?:$/i.test(url.protocol) || url.origin !== base.origin)
	    return null;
	  const segments = url.pathname.split("/").filter(Boolean);
	  if (segments[0] !== "t") return null;
	  const numericFirst = (0, import_identifiers.tryDiscourseTopicId)(segments[1]), topicId = numericFirst ?? (0, import_identifiers.tryDiscourseTopicId)(segments[2]);
	  if (!topicId) return null;
	  const postValue = segments[numericFirst ? 2 : 3], postNumber = postValue === void 0 ? null : (0, import_identifiers.tryDiscoursePostNumber)(postValue);
	  return postValue !== void 0 && !postNumber ? null : Object.freeze({
	    topicId,
	    postNumber,
	    bypassReader: url.searchParams.has(
	      import_reader_native_topic_route.READER_NATIVE_BYPASS_PARAMETER
	    ),
	    href: url.href
	  });
	}
	function parseReaderUserscriptUserRoute(value, baseValue) {
	  let base, url;
	  try {
	    base = new URL(baseValue), url = new URL(value, base);
	  } catch {
	    return null;
	  }
	  if (!/^https?:$/i.test(url.protocol) || url.origin !== base.origin)
	    return null;
	  const segments = url.pathname.split("/").filter(Boolean);
	  if (segments[0] !== "u" || !segments[1]) return null;
	  try {
	    const username = decodeURIComponent(segments[1]).trim().replace(/^@+/, "");
	    return username ? Object.freeze({ username }) : null;
	  } catch {
	    return null;
	  }
	}
	function readerUserscriptRouteKind(value, baseValue) {
	  return parseReaderUserscriptTopicRoute(value, baseValue) ? "direct-topic" : "list";
	}
	function directText(element2) {
	  return element2 ? [...element2.childNodes].filter((node) => node.nodeType === 3).map((node) => String(node.textContent ?? "").trim()).filter(Boolean).join(" ").trim() : "";
	}
	function firstProfileElement(documentPort, selectors) {
	  for (const selector of selectors) {
	    const candidate = documentPort.querySelector(selector);
	    if (candidate) return candidate;
	  }
	  return null;
	}
	function userRouteFromUsername(value) {
	  const username = String(value ?? "").trim().replace(/^@+/, "");
	  return username && !/[\s/]/u.test(username) ? Object.freeze({ username }) : null;
	}
	function hostDocumentUserRoute(documentPort, currentUrl) {
	  const userMain = documentPort.querySelector(".user-main");
	  if (!userMain) return null;
	  const dataUsername = userMain.matches("[data-username]") ? userMain : userMain.querySelector("[data-username]"), dataRoute = userRouteFromUsername(
	    dataUsername?.getAttribute("data-username")
	  );
	  if (dataRoute) return dataRoute;
	  const username = firstProfileElement(
	    documentPort,
	    HOST_USER_PROFILE_USERNAME_SELECTORS
	  ), textRoute = userRouteFromUsername(directText(username));
	  if (textRoute) return textRoute;
	  const profileLink = userMain.querySelector('a[href*="/u/"]'), baseUrl = currentUrl || documentPort.baseURI;
	  return profileLink && baseUrl ? parseReaderUserscriptUserRoute(profileLink.href, baseUrl) : null;
	}
	class ReaderUserscriptUserObservationEntry {
	  scope;
	  #options;
	  #retryTimers = /* @__PURE__ */ new Set();
	  #routeEpoch = 0;
	  constructor(options) {
	    this.#options = options, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const onClick = (event) => this.#handleClick(event);
	    if (options.document.addEventListener("click", onClick, !0), this.scope.add(() => {
	      options.document.removeEventListener("click", onClick, !0);
	    }), options.routeChanges)
	      try {
	        this.scope.add(options.routeChanges.subscribe(() => {
	          this.syncCurrentRoute();
	        }));
	      } catch (error) {
	        this.#report(error);
	      }
	    if (options.hostMutations)
	      try {
	        this.scope.add(options.hostMutations.subscribe(() => {
	          this.syncCurrentRoute();
	        }));
	      } catch (error) {
	        this.#report(error);
	      }
	    this.scope.add(() => {
	      this.#routeEpoch += 1, this.#cancelRetries(), this.#removeEntries();
	    }), this.syncCurrentRoute();
	  }
	  syncCurrentRoute() {
	    if (this.scope.destroyed) return !1;
	    const epoch = ++this.#routeEpoch;
	    this.#cancelRetries();
	    let currentUrl = "";
	    try {
	      currentUrl = String(this.#options.currentUrl()).trim();
	    } catch (error) {
	      this.#report(error);
	    }
	    const route = (currentUrl ? parseReaderUserscriptUserRoute(currentUrl, currentUrl) : null) ?? hostDocumentUserRoute(
	      this.#options.document,
	      currentUrl
	    );
	    if (!route)
	      return this.#removeEntries(), !1;
	    if (this.#mount(route)) return !0;
	    for (const delay of HOST_USER_PROFILE_RETRY_DELAYS) {
	      const timer = setTimeout(() => {
	        this.#retryTimers.delete(timer), !(this.scope.destroyed || epoch !== this.#routeEpoch) && this.#mount(route) && this.#cancelRetries();
	      }, delay);
	      this.#retryTimers.add(timer);
	    }
	    return !1;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #mount(route) {
	    const name = firstProfileElement(
	      this.#options.document,
	      HOST_USER_PROFILE_NAME_SELECTORS
	    );
	    if (!name) return !1;
	    if (name.querySelector(
	      HOST_USER_OBSERVATION_ENTRY_SELECTOR
	    )?.dataset.readerUserObservationUsername === route.username) return !0;
	    this.#removeEntries();
	    const secondary = name.closest(".user-profile-names")?.querySelector(
	      ".user-profile-names__secondary"
	    ) ?? null, primaryText = directText(name), secondaryText = directText(secondary), displayName = [primaryText, secondaryText].find((candidate) => candidate && candidate.replace(/^@+/, "").toLocaleLowerCase() !== route.username.toLocaleLowerCase()) ?? "", avatar = this.#options.document.querySelector(
	      ".user-main .user-profile-avatar img,.user-main .avatar-wrapper img,.user-main img.avatar"
	    ), button = this.#options.document.createElement("button");
	    button.type = "button", button.className = "ldp-host-user-observation-entry", button.dataset.readerUserObservationUsername = route.username, button.dataset.readerUserObservationName = displayName, button.dataset.readerUserObservationAvatar = avatar?.getAttribute("data-avatar-template") ?? avatar?.getAttribute("src") ?? "", button.setAttribute("aria-label", `用户观察:@${route.username}`), button.title = "用户观察", button.append((0, import_reader_icon.createReaderIcon)(this.#options.document, "activity"));
	    const label = this.#options.document.createElement("span");
	    return label.className = "ldp-host-user-observation-entry-label", label.textContent = "观察用户", button.append(label), directText(name) ? name.insertBefore(button, name.firstElementChild) : name.append(button), !0;
	  }
	  #handleClick(event) {
	    const target = element(event.target)?.closest(
	      HOST_USER_OBSERVATION_ENTRY_SELECTOR
	    ) ?? null;
	    if (!target || target.disabled || this.scope.destroyed) return;
	    event.preventDefault(), event.stopPropagation(), event.stopImmediatePropagation();
	    const username = String(
	      target.dataset.readerUserObservationUsername ?? ""
	    ).trim();
	    if (!username) return;
	    target.disabled = !0, target.setAttribute("aria-busy", "true");
	    const identity = Object.freeze({
	      username,
	      name: String(target.dataset.readerUserObservationName ?? "").trim(),
	      avatarTemplate: String(
	        target.dataset.readerUserObservationAvatar ?? ""
	      ).trim()
	    });
	    Promise.resolve().then(() => this.#options.openObservation(identity)).catch((error) => {
	      this.#report(error);
	    }).finally(() => {
	      target.isConnected && (target.disabled = !1, target.removeAttribute("aria-busy"));
	    });
	  }
	  #removeEntries() {
	    for (const entry of this.#options.document.querySelectorAll(
	      HOST_USER_OBSERVATION_ENTRY_SELECTOR
	    )) entry.remove();
	  }
	  #cancelRetries() {
	    for (const timer of this.#retryTimers) clearTimeout(timer);
	    this.#retryTimers.clear();
	  }
	  #report(error) {
	    try {
	      this.#options.onError?.(error);
	    } catch {
	    }
	  }
	}
	class ReaderUserscriptTargetAdapter {
	  scope;
	  ready;
	  #options;
	  #onClick;
	  #routeEpoch = 0;
	  #targetEpoch = 0;
	  #lastRouteKey = "";
	  constructor(options) {
	    if (this.#options = options, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#onClick = (event) => {
	      this.#handleClick(event);
	    }, options.interceptTopicLinks !== !1 && (options.document.addEventListener("click", this.#onClick, !0), this.scope.add(() => {
	      options.document.removeEventListener("click", this.#onClick, !0);
	    })), options.routeChanges)
	      try {
	        this.scope.add(options.routeChanges.subscribe(() => {
	          this.syncCurrentRoute();
	        }));
	      } catch (error) {
	        this.#report(error);
	      }
	    if (options.serviceWorkerMessages) {
	      const listener = (event) => {
	        this.#handleServiceWorkerMessage(event);
	      };
	      options.serviceWorkerMessages.addEventListener(
	        "message",
	        listener,
	        !0
	      ), this.scope.add(() => {
	        options.serviceWorkerMessages?.removeEventListener(
	          "message",
	          listener,
	          !0
	        );
	      });
	    }
	    this.scope.add(() => {
	      this.#routeEpoch += 1, this.#targetEpoch += 1, this.#lastRouteKey = "";
	    }), options.openInitialRoute === !1 ? (this.#rememberCurrentRoute(), this.ready = Promise.resolve(!1)) : this.ready = this.syncCurrentRoute({ force: !0 });
	  }
	  async syncCurrentRoute(options = {}) {
	    if (this.scope.destroyed) return !1;
	    const currentUrl = this.#currentUrl();
	    if (!currentUrl) return !1;
	    const route = parseReaderUserscriptTopicRoute(
	      currentUrl,
	      currentUrl
	    );
	    if (!route || route.bypassReader)
	      return this.#lastRouteKey = "", !1;
	    const ordinaryTarget = this.#ordinaryTarget(route), targetPostNumber = ordinaryTarget.postNumber, routeKey = `${route.topicId}:${targetPostNumber ?? 0}`;
	    if (options.force !== !0 && routeKey === this.#lastRouteKey)
	      return !1;
	    this.#lastRouteKey = routeKey;
	    const epoch = ++this.#routeEpoch, targetEpoch = ++this.#targetEpoch, opened = await this.#open({
	      topicId: route.topicId,
	      ...targetPostNumber === null ? {} : { postNumber: targetPostNumber },
	      ...ordinaryTarget.fromHistory ? { alignment: "start" } : {},
	      source: "restore"
	    }, ordinaryTarget.fromHistory);
	    return epoch !== this.#routeEpoch || targetEpoch !== this.#targetEpoch || this.scope.destroyed ? !1 : (opened || (this.#lastRouteKey = ""), opened);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #handleClick(event) {
	    if (this.scope.destroyed || !isPlainPrimaryClick(event)) return;
	    const anchor = eventAnchor(event) ?? eventHostTopicCardAnchor(event);
	    if (!anchor || anchor.closest(BYPASS_SELECTOR) || anchor.hasAttribute("download") || anchor.getAttribute("aria-disabled") === "true" || anchor.closest(".search-menu") && !anchor.closest(
	      ".search-result-topic,.search-result-post," + SOURCE_SELECTOR
	    ))
	      return;
	    const target = String(anchor.getAttribute("target") ?? "").toLowerCase();
	    if (target && target !== "_self") return;
	    const linkTarget = this.#linkTarget(anchor);
	    if (!linkTarget || linkTarget.route.bypassReader) return;
	    const nativeNotificationSource = nativeHostNotificationSource(anchor);
	    event.preventDefault(), event.stopPropagation(), event.stopImmediatePropagation();
	    const source = nativeNotificationSource ?? linkTarget.source, ordinaryTarget = source === "link" && !linkTarget.preservePostNumber ? this.#ordinaryTarget(linkTarget.route) : null, postNumber = ordinaryTarget?.postNumber ?? linkTarget.route.postNumber, request = {
	      topicId: linkTarget.route.topicId,
	      ...postNumber === null ? {} : { postNumber },
	      ...ordinaryTarget?.fromHistory === !0 ? { alignment: "start" } : {},
	      source
	    };
	    this.#openIntercepted({
	      request,
	      historical: ordinaryTarget?.fromHistory === !0,
	      anchor,
	      sourceElement: linkTarget.sourceElement,
	      pointer: Number.isFinite(event.clientY) ? Object.freeze({
	        clientY: event.clientY,
	        detail: Number(event.detail) || 0
	      }) : null
	    });
	  }
	  #handleServiceWorkerMessage(event) {
	    if (this.scope.destroyed) return;
	    const data = event.data;
	    if (data === null || typeof data != "object") return;
	    const targetUrl = String(
	      data.url ?? ""
	    ).trim();
	    if (!targetUrl) return;
	    const currentUrl = this.#currentUrl();
	    if (!currentUrl) return;
	    const route = parseReaderUserscriptTopicRoute(targetUrl, currentUrl);
	    !route || route.bypassReader || (event.preventDefault(), event.stopPropagation(), event.stopImmediatePropagation(), this.#targetEpoch += 1, this.#open({
	      topicId: route.topicId,
	      ...route.postNumber === null ? {} : { postNumber: route.postNumber },
	      source: "notification"
	    }));
	  }
	  #linkTarget(anchor) {
	    const currentUrl = this.#currentUrl();
	    if (!currentUrl) return null;
	    const href = anchor.getAttribute("href") ?? "";
	    if (!isSameOriginHttpTarget(href, currentUrl)) return null;
	    const fallback = parseReaderUserscriptTopicRoute(href, currentUrl), marker = anchor.closest(SOURCE_SELECTOR), route = markerRoute(marker, fallback);
	    if (!route) return null;
	    const source = linkSource(marker), preservePostNumber = source !== "link" || truthyAttribute(anchor, "data-reader-preserve-target") || truthyAttribute(marker, "data-reader-preserve-target") || truthyAttribute(anchor, "data-ldp-preserve-target-post") || truthyAttribute(marker, "data-ldp-preserve-target-post");
	    return Object.freeze({
	      route,
	      source,
	      preservePostNumber,
	      sourceElement: marker
	    });
	  }
	  #ordinaryPostNumber(route) {
	    return this.#ordinaryTarget(route).postNumber;
	  }
	  #ordinaryTarget(route) {
	    try {
	      const historical = this.#options.readHistoryPostNumber?.(route.topicId) ?? null;
	      return Object.freeze(historical !== null ? {
	        postNumber: historical,
	        fromHistory: !0
	      } : {
	        postNumber: this.#options.readOpenTopicsAtFirstPost?.() === !0 ? (0, import_identifiers.tryDiscoursePostNumber)(1) : route.postNumber,
	        fromHistory: !1
	      });
	    } catch (error) {
	      return this.#report(error), Object.freeze({
	        postNumber: route.postNumber,
	        fromHistory: !1
	      });
	    }
	  }
	  #rememberCurrentRoute() {
	    const currentUrl = this.#currentUrl();
	    if (!currentUrl) return;
	    const route = parseReaderUserscriptTopicRoute(currentUrl, currentUrl);
	    !route || route.bypassReader || (this.#lastRouteKey = `${route.topicId}:${this.#ordinaryPostNumber(route) ?? 0}`);
	  }
	  #currentUrl() {
	    try {
	      const value = String(this.#options.currentUrl()).trim();
	      if (!value) throw new Error("当前页面 URL 为空");
	      return value;
	    } catch (error) {
	      return this.#report(error), null;
	    }
	  }
	  #report(error) {
	    try {
	      this.#options.onError?.(error);
	    } catch {
	    }
	  }
	  async #openIntercepted(target) {
	    const epoch = ++this.#targetEpoch;
	    if (this.#options.beforeOpenTarget)
	      try {
	        await this.#options.beforeOpenTarget(target);
	      } catch (error) {
	        this.#report(error);
	      }
	    if (this.scope.destroyed || epoch !== this.#targetEpoch)
	      return await this.#settleIntercepted(target, !1), !1;
	    const opened = await this.#open(target.request, target.historical);
	    return this.scope.destroyed || epoch !== this.#targetEpoch ? (await this.#settleIntercepted(target, !1), !1) : (await this.#settleIntercepted(target, opened), opened);
	  }
	  async #settleIntercepted(target, opened) {
	    if (this.#options.afterOpenTarget)
	      try {
	        await this.#options.afterOpenTarget(target, opened);
	      } catch (error) {
	        this.#report(error);
	      }
	  }
	  async #open(request, historical = !1) {
	    try {
	      const result = historical && this.#options.target.openHistoricalTarget ? await this.#options.target.openHistoricalTarget(request) : await this.#options.target.openTarget(request);
	      if (result.topic.status === "opened" || result.topic.status === "reused")
	        return request.postNumber === void 0 || result.navigation?.status === "revealed";
	      if (result.topic.status === "failed")
	        throw result.topic.cause ?? new Error(`Reader 目标 Topic ${request.topicId} 打开失败`);
	      return !1;
	    } catch (error) {
	      return this.#report(error), !1;
	    }
	  }
	}
}, "d0cfa770d3ccfb08d6cfadbd170481a856e6e17ee4452ad5ad98d70aeedddfce");

	runtime.markLibrary("main-lite-core");
})();