Notion标题自动编号

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

当前为 2025-08-10 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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 标题名称