Notion标题自动编号

为Notion页面中的标题(H1-H3)自动添加编号,支持目录同步更新

目前為 2025-08-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

作者
gou tan
評價
0 0 0
版本
1.0.4
建立日期
2025-08-09
更新日期
2025-08-10
尺寸
6.9 KB
授權條款
MIT
腳本執行於

Notion优化(持续更新中)

1. Notion标题和Notion Boost目录自动编号

📅 2025-08-09 有效

1.1. 实现原理

通过注入 User JavaScript 代码实现标题和目录自动编号,采用纯前端渲染方式,不改变页面本质内容。

Quickstart: 油猴脚本 (Notion标题自动编号),安装后,在Notion页面刷新即可生效。

1.2. 所需插件

1.3. 操作步骤

  1. 进入 Notion Boost 插件,打开 Show Outline 功能
  2. 进入 User JavaScript and CSS 插件,创建新规则
  3. 添加以下配置内容并保存,返回 Notion 页面刷新即可生效

1.4. 配置内容

URL pattern: https://www.notion.so/*

User JavaScript 代码:

// ==UserScript==
// @name         Notion标题自动编号
// @match        https://www.notion.so/*
// @run-at       document-start
// ==/UserScript==

(() => {
    // 创建并注入基础样式
    const style = document.createElement("style");
    style.id = "nb-style";
    style.textContent = `
      .notion-page-content [placeholder^="Heading"] { position: relative; }
      .notion-page-content [placeholder^="Heading"]::before,
      .table_of_contents .text::before {
        content: "";
        margin-right: .2em;
        pointer-events: none;
        user-select: none;
        font-weight: bold;
      }
      .notion-page-content [placeholder^="Heading"]::before { color: #37352F; }
      h1[placeholder="New page"]::before,
      h1[placeholder*="Untitled"]::before { content: "" !important; }
    `;

    // 确保样式已注入
    const ensureStyle = () => {
      if (!document.getElementById("nb-style")) {
        (document.head || document.documentElement).appendChild(style);
      }
    };

    // 工具函数简化
    const q = (sel, root = document) => root.querySelector(sel);
    const qa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
    const hyphenate32 = s => s && s.length === 32 
      ? s.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5") 
      : s;

    // 获取目录列表
    const getToCList = () => {
      const toc = q(".table_of_contents");
      if (!toc) return [];

      return qa(".block[hash]", toc).reduce((acc, block) => {
        const hash = block.getAttribute("hash");
        if (!hash || hash.length !== 32) return acc;

        const align = block.querySelector(".align");
        let level = 0;
        if (align?.classList.contains("nb-h1")) level = 1;
        else if (align?.classList.contains("nb-h2")) level = 2;
        else if (align?.classList.contains("nb-h3")) level = 3;

        if (level) {
          acc.push({ 
            id: hyphenate32(hash), 
            idRaw: hash, 
            level 
          });
        }
        return acc;
      }, []);
    };

    // 获取页面标题
    const getMountedHeadings = () => {
      const seen = new Set();
      return qa(
        '.notion-page-content [placeholder="Heading 1"], ' +
        '.notion-page-content [placeholder="Heading 2"], ' +
        '.notion-page-content [placeholder="Heading 3"]'
      ).reduce((acc, el) => {
        if (el.closest('[aria-hidden="true"]')) return acc;
        const block = el.closest("[data-block-id]");
        if (!block) return acc;

        const id = block.getAttribute("data-block-id");
        if (!id || seen.has(id)) return acc;
        seen.add(id);

        const ph = el.getAttribute("placeholder") || "";
        const level = ph === "Heading 1" ? 1 : ph === "Heading 2" ? 2 : 3;
        acc.push({ id, idRaw: id.replace(/-/g, ""), level });
        return acc;
      }, []);
    };

    // 生成编号映射
    const buildNumbering = items => {
      let [c1, c2, c3] = [0, 0, 0];
      const map = new Map();

      items.forEach(it => {
        if (it.level === 1) {
          [c1, c2, c3] = [c1 + 1, 0, 0];
          map.set(it.id, `${c1}. `);
        } else if (it.level === 2) {
          [c2, c3] = [c2 + 1, 0];
          map.set(it.id, `${c1}.${c2} `);
        } else {
          c3++;
          map.set(it.id, `${c1}.${c2}.${c3} `);
        }
      });
      return map;
    };

    // 应用编号样式
    const applyNumbering = (numMap, tocItems) => {
      const rules = [];

      // 应用标题编号
      numMap.forEach((label, id) => {
        rules.push(`[data-block-id="${id}"] [placeholder^="Heading"]::before { content: "${label}"; }`);
      });

      // 应用目录编号
      tocItems.forEach(item => {
        const label = numMap.get(item.id);
        if (label) {
          rules.push(`.table_of_contents .block[hash="${item.idRaw}"] .text::before { content: "${label}"; }`);
        }
      });

      // 更新样式
      style.textContent = style.textContent.split('\n').slice(0, 12).join('\n') + rules.join('\n');
    };

    // 主逻辑
    let scheduled = false;
    const rebuild = () => {
      ensureStyle();
      const tocItems = getToCList();
      const source = tocItems.length ? tocItems : getMountedHeadings();

      if (source.length) {
        const numMap = buildNumbering(source);
        applyNumbering(numMap, tocItems);
      }
    };

    // 防抖处理
    const schedule = () => {
      if (!scheduled) {
        scheduled = true;
        setTimeout(() => {
          scheduled = false;
          rebuild();
        }, 120);
      }
    };

    // 启动观察器
    const startObservers = () => {
      // 优化观察器配置,减少不必要的监听
      new MutationObserver(schedule).observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['hash', 'data-block-id', 'placeholder']
      });

      rebuild();
      setTimeout(rebuild, 800);
      setInterval(rebuild, 3000);
    };

    // 初始化
    ensureStyle();
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", startObservers, { once: true });
    } else {
      startObservers();
    }
  })();

1.5. 效果展示

  • 一级标题:1. 标题名称
  • 二级标题:1.1 标题名称
  • 三级标题:1.1.1 标题名称