LINUX DO TOC

对 DiscoTOC 的 toc-processor 做运行时补丁:有标题就自动视为开启 TOC

2026-02-15 기준 버전입니다. 최신 버전을 확인하세요.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         LINUX DO TOC
// @namespace    https://linux.do/
// @version      0.2.0
// @description  对 DiscoTOC 的 toc-processor 做运行时补丁:有标题就自动视为开启 TOC
// @match        https://linux.do/t/*
// @match        https://linux.do/*/t/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  "use strict";

  const STATE_KEY = "__linuxdo_auto_toc_patched__";

  function getContainer() {
    try {
      const app = window.require?.("discourse/app")?.default;
      if (app?.__container__) return app.__container__;
    } catch {}
    try {
      if (window.Discourse?.__container__) return window.Discourse.__container__;
    } catch {}
    return null;
  }

  function refreshTOC(container) {
    try {
      const toc = container.lookup?.("service:toc-processor");
      const topicController = container.lookup?.("controller:topic");
      if (toc && topicController?.model) {
        toc.checkPostforTOC(topicController.model);
      }
    } catch {}
  }

  function patchOnce() {
    if (window[STATE_KEY]) return true;

    const container = getContainer();
    if (!container) return false;

    const toc = container.lookup?.("service:toc-processor");
    if (!toc || typeof toc.containsTocMarkup !== "function") return false;

    if (toc[STATE_KEY]) {
      window[STATE_KEY] = true;
      refreshTOC(container);
      return true;
    }

    const originalContains = toc.containsTocMarkup.bind(toc);
    toc.containsTocMarkup = function (content) {
      if (originalContains(content)) return true;
      if (typeof this.containsHeadings === "function" && this.containsHeadings(content)) {
        return true;
      }
      return false;
    };

    if (typeof toc.containsHeadings === "function") {
      const originalHasHeadings = toc.containsHeadings.bind(toc);
      toc.containsHeadings = function (content) {
        return originalHasHeadings(content) || content.includes("<h6");
      };
    }

    toc[STATE_KEY] = true;
    window[STATE_KEY] = true;

    refreshTOC(container);
    setTimeout(() => refreshTOC(container), 300);
    setTimeout(() => refreshTOC(container), 1200);

    return true;
  }

  function hookNavigation() {
    if (window.__linuxdo_auto_toc_nav_hooked__) return;
    window.__linuxdo_auto_toc_nav_hooked__ = true;

    const schedule = () => setTimeout(patchOnce, 0);

    const origPush = history.pushState;
    history.pushState = function () {
      const res = origPush.apply(this, arguments);
      schedule();
      return res;
    };

    const origReplace = history.replaceState;
    history.replaceState = function () {
      const res = origReplace.apply(this, arguments);
      schedule();
      return res;
    };

    window.addEventListener("popstate", schedule, true);
    document.addEventListener("DOMContentLoaded", schedule, { once: true });
  }

  hookNavigation();

  const mo = new MutationObserver(() => patchOnce());
  mo.observe(document.documentElement, { childList: true, subtree: true });

  patchOnce();
})();