为Notion页面中的标题(H1-H3)自动添加编号,支持目录同步更新
当前为
Notion优化(持续更新中)
📅 2025-08-09 有效
通过注入 User JavaScript 代码实现标题和目录自动编号,采用纯前端渲染方式,不改变页面本质内容。
Quickstart: 油猴脚本 (Notion标题自动编号),安装后,在Notion页面刷新即可生效。
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();
}
})();