您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为Notion页面标题添加视觉编号效果,纯JS实现,不修改原始内容,支持目录同步更新
// ==UserScript== // @name Notion标题视觉编号工具 // @namespace https://floritange.github.io/ // @version 1.0.6 // @description 为Notion页面标题添加视觉编号效果,纯JS实现,不修改原始内容,支持目录同步更新 // @author goutan // @match https://www.notion.so/* // @grant GM_addStyle // @run-at document-end // @license Apache-2.0 // @noframes // @icon https://www.notion.so/front-static/favicon.ico // ==/UserScript== (function () { "use strict"; // 使用 GM_addStyle 注入基础样式 GM_addStyle(` .notion-page-content [placeholder^="Heading"] { position: relative; } .notion-page-content [placeholder^="Heading"]::before, .table_of_contents .text::before { content: ""; margin-right: .1em; 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 dynamicStyle = document.createElement("style"); dynamicStyle.id = "notion-numbering-dynamic"; // 确保动态样式已注入 const ensureDynamicStyle = () => { if (!document.getElementById("notion-numbering-dynamic")) { (document.head || document.documentElement).appendChild(dynamicStyle); } }; // DOM查询工具函数 const querySelector = (selector, root = document) => root.querySelector(selector); const querySelectorAll = (selector, root = document) => Array.from(root.querySelectorAll(selector)); // 32位字符串转UUID格式 const formatToUUID = (str) => { if (!str || str.length !== 32) return str; return str.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5"); }; // 从目录获取标题列表 const getTableOfContentsItems = () => { const tocContainer = querySelector(".table_of_contents"); if (!tocContainer) return []; return querySelectorAll(".block[hash]", tocContainer).reduce( (accumulator, block) => { const hashValue = block.getAttribute("hash"); if (!hashValue || hashValue.length !== 32) return accumulator; const alignElement = block.querySelector(".align"); let headingLevel = 0; // 检测标题级别 if (alignElement?.classList.contains("nb-h1")) headingLevel = 1; else if (alignElement?.classList.contains("nb-h2")) headingLevel = 2; else if (alignElement?.classList.contains("nb-h3")) headingLevel = 3; if (headingLevel > 0) { accumulator.push({ id: formatToUUID(hashValue), rawId: hashValue, level: headingLevel, }); } return accumulator; }, [] ); }; // 从页面内容获取标题元素 const getPageHeadings = () => { const processedIds = new Set(); const headingSelectors = [ '.notion-page-content [placeholder="Heading 1"]', '.notion-page-content [placeholder="Heading 2"]', '.notion-page-content [placeholder="Heading 3"]', ].join(", "); return querySelectorAll(headingSelectors).reduce((accumulator, element) => { // 跳过隐藏元素 if (element.closest('[aria-hidden="true"]')) return accumulator; const blockElement = element.closest("[data-block-id]"); if (!blockElement) return accumulator; const blockId = blockElement.getAttribute("data-block-id"); if (!blockId || processedIds.has(blockId)) return accumulator; processedIds.add(blockId); const placeholder = element.getAttribute("placeholder") || ""; let level = 0; if (placeholder === "Heading 1") level = 1; else if (placeholder === "Heading 2") level = 2; else if (placeholder === "Heading 3") level = 3; if (level > 0) { accumulator.push({ id: blockId, rawId: blockId.replace(/-/g, ""), level, }); } return accumulator; }, []); }; // 生成标题编号 const generateNumbering = (headingItems) => { const counters = [0, 0, 0]; // [H1, H2, H3] 计数器 const numberingMap = new Map(); headingItems.forEach((item) => { if (item.level === 1) { counters[0]++; counters[1] = 0; counters[2] = 0; numberingMap.set(item.id, `${counters[0]}. `); } else if (item.level === 2) { counters[1]++; counters[2] = 0; numberingMap.set(item.id, `${counters[0]}.${counters[1]} `); } else if (item.level === 3) { counters[2]++; numberingMap.set( item.id, `${counters[0]}.${counters[1]}.${counters[2]} ` ); } }); return numberingMap; }; // 应用编号样式 const applyNumberingStyles = (numberingMap, tocItems) => { const cssRules = []; // 为页面标题生成CSS规则 numberingMap.forEach((numberLabel, blockId) => { cssRules.push( `[data-block-id="${blockId}"] [placeholder^="Heading"]::before { content: "${numberLabel}"; }` ); }); // 为目录标题生成CSS规则 tocItems.forEach((item) => { const numberLabel = numberingMap.get(item.id); if (numberLabel) { cssRules.push( `.table_of_contents .block[hash="${item.rawId}"] .text::before { content: "${numberLabel}"; }` ); } }); // 更新动态样式 dynamicStyle.textContent = cssRules.join("\n"); }; // 防抖控制 let isScheduled = false; // 主要重建逻辑 const rebuildNumbering = () => { ensureDynamicStyle(); const tocItems = getTableOfContentsItems(); const sourceItems = tocItems.length > 0 ? tocItems : getPageHeadings(); if (sourceItems.length > 0) { const numberingMap = generateNumbering(sourceItems); applyNumberingStyles(numberingMap, tocItems); } }; // 防抖调度器 const scheduleRebuild = () => { if (!isScheduled) { isScheduled = true; setTimeout(() => { isScheduled = false; rebuildNumbering(); }, 120); } }; // 启动观察器 const initializeObservers = () => { // DOM变化观察器 const observer = new MutationObserver(scheduleRebuild); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["hash", "data-block-id", "placeholder"], }); // 立即执行一次 rebuildNumbering(); // 延迟执行,确保页面完全加载 setTimeout(rebuildNumbering, 800); // 定期检查更新 setInterval(rebuildNumbering, 3000); }; // 脚本初始化 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeObservers, { once: true, }); } else { initializeObservers(); } })();