PageSourceSearch

https://notion-next-tan-omega.vercel.app/js/spoilerText.js

js notion-next-tan-omega.vercel.app collected 2026-10-03 07:24:43 UTC 3,993 bytes, 140 lines download raw bytes

1/**
2 * 转义正则表达式中的特殊字符
3 * @param {string} string 需要转义的字符串
4 * @returns {string} 转义后的字符串
5 */
6function escapeRegExp(string) {
7  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
8}
9
10/**
11 * 将Node文本中的指定标签内容转换为带有指定类名的span
12 * @param regex
13 * @param node
14 * @param className
15 */
16function convertTextToSpoilerSpan(regex, node, className) {
17  // 使用 textContent 替代 wholeText 以确保类型安全
18  const textContent = node.textContent
19  let outerSpan = document.createElement('span')
20  const fragments = []
21  let lastIndex = 0
22  let match
23  while ((match = regex.exec(textContent)) !== null) {
24    console.log('符合要求的文字' + textContent)
25    // 添加前面未匹配的部分
26    if (match.index > lastIndex) {
27      outerSpan.appendChild(
28        document.createTextNode(textContent.slice(lastIndex, match.index))
29      )
30    }
31
32    // 创建 span 包裹的内容
33    const span = document.createElement('span')
34    span.textContent = match[1] // 提取匹配的内容
35    if (className) {
36      span.className = className
37    }
38    outerSpan.appendChild(span)
39    // 设置lastIndex
40    lastIndex = regex.lastIndex
41  }
42  if (outerSpan.childNodes.length) {
43    // 添加剩余未匹配的部分
44    if (lastIndex < textContent.length) {
45      outerSpan.appendChild(
46        document.createTextNode(textContent.slice(lastIndex))
47      )
48    }
49    node.replaceWith(outerSpan)
50  }
51}
52
53/**
54 * 收集并处理指定节点下的所有文本节点
55 * @param root
56 * @param className
57 * @param spoilerTag
58 */
59function processTextNodes(root, className, spoilerTag) {
60  const regex = new RegExp(`${spoilerTag}(.*?)${spoilerTag}`, 'g')
61  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
62    acceptNode: function (node) {
63      if (node.nodeType === Node.TEXT_NODE) {
64        return regex.test(node.textContent)
65          ? NodeFilter.FILTER_ACCEPT
66          : NodeFilter.FILTER_REJECT
67      }
68      return NodeFilter.FILTER_REJECT
69    }
70  })
71  const waitProcessNodes = []
72  while (walker.nextNode()) {
73    const node = walker.currentNode
74    waitProcessNodes.push(node)
75  }
76  for (const waitProcessNode of waitProcessNodes) {
77    convertTextToSpoilerSpan(regex, waitProcessNode, className)
78  }
79
80  // 处理跨节点的 spoiler 标记
81  processCrossNodeSpoilers(root, className, spoilerTag)
82}
83
84/**
85 * 处理跨节点的 spoiler 标记
86 * @param {Element} root 要处理的根元素
87 * @param {string} className 应用于 spoiler 内容的类名
88 * @param {string} spoilerTag spoiler 标记符号
89 */
90function processCrossNodeSpoilers(root, className, spoilerTag) {
91  if (root.nodeType !== Node.ELEMENT_NODE) return
92
93  const html = root.innerHTML
94
95  // 处理原始标签,如果是已经转义过的,则去除转义
96  let originalTag = spoilerTag
97  if (spoilerTag.startsWith('\\') || spoilerTag.includes('\\[')) {
98    originalTag = spoilerTag.replace(/\\/g, '')
99  }
100
101  // 创建正则表达式,直接匹配原始标签
102  const regex = new RegExp(`\\${originalTag}([\\s\\S]*?)\\${originalTag}`, 'g')
103
104  const hasMatch = regex.test(html)
105
106  if (!hasMatch) return
107
108  // 重置正则表达式
109  regex.lastIndex = 0
110
111  // 替换匹配项
112  const newHtml = html.replace(regex, function (match, content) {
113    return `<span class="${className}">${content}</span>`
114  })
115
116  // 如果内容有变化,更新 DOM
117  if (newHtml !== html) {
118    root.innerHTML = newHtml
119  }
120}
121
122/**
123 * 定位到目标处理位置,开始进行文本到spoiler的转换
124 * @param spoilerTag
125 */
126function textToSpoiler(spoilerTag) {
127  const intervalID = setInterval(() => {
128    const articleElement = document.querySelector(
129      '#article-wrapper #notion-article main'
130    )
131    if (articleElement) {
132      setTimeout(() => {
133        processTextNodes(articleElement, 'spoiler-text', spoilerTag)
134        clearInterval(intervalID)
135      }, 300)
136    }
137  }, 1000)
138}
139
140window.textToSpoiler = textToSpoiler

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.