Bilibili-Evolved/registry/lib/components/utils/active-video-links/index.ts
2025-04-27 22:49:47 +08:00

152 lines
4.2 KiB
TypeScript

import { defineComponentMetadata } from '@/components/define'
import { useScopedConsole } from '@/core/utils/log'
import { videoUrls } from '@/core/utils/urls'
const console = useScopedConsole('activeVideoLinks')
let observer: MutationObserver | null = null
const webRegex = /(?<!(\>|'|"|\/))(http:\/\/|https:\/\/|www\.)[^(\s,;())]+/g
const processAcgLinks = () => {
const links = document.querySelectorAll('a[href*="acg.tv"][href*="sm"]')
links.forEach(link => {
const originalHref = link.getAttribute('href')
if (originalHref) {
const newHref = originalHref.replace('acg.tv', 'nicovideo.jp/watch')
link.setAttribute('href', newHref)
console.log(`Niconico Fix: ${originalHref}${newHref}`)
}
})
}
const processDescLinks = () => {
const descContainer = document.querySelector('.desc-info-text')
if (!descContainer) {
return
}
const walker = document.createTreeWalker(descContainer, NodeFilter.SHOW_TEXT, {
acceptNode: node => {
if (node.parentElement?.closest('a')) {
return NodeFilter.FILTER_REJECT
}
return webRegex.test(node.textContent || '')
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT
},
})
const textNodes: Text[] = []
let currentNode: Node | null = walker.nextNode()
while (currentNode !== null) {
textNodes.push(currentNode as Text)
currentNode = walker.nextNode()
}
textNodes.forEach(textNode => {
const frag = document.createDocumentFragment()
const text = textNode.textContent || ''
let lastIndex = 0
let match: RegExpExecArray | null
webRegex.lastIndex = 0
match = webRegex.exec(text)
while (match !== null) {
const start = match.index
if (lastIndex < start) {
frag.appendChild(document.createTextNode(text.slice(lastIndex, start)))
}
const urlText = match[0]
const linkHref = urlText.replace(/^https?:\/\//, '//').replace(/^www\./, '//')
const a = document.createElement('a')
a.href = linkHref
a.target = '_blank'
a.textContent = urlText
frag.appendChild(a)
lastIndex = start + urlText.length
match = webRegex.exec(text)
}
if (lastIndex < text.length) {
frag.appendChild(document.createTextNode(text.slice(lastIndex)))
}
textNode.parentNode?.replaceChild(frag, textNode)
})
}
const normalizeNicoDescLinks = () => {
const descContainer = document.querySelector('.desc-info-text')
if (!descContainer) {
return
}
const anchors = Array.from(descContainer.querySelectorAll('a'))
for (let i = 0; i < anchors.length - 1; i++) {
const a1 = anchors[i]
const a2 = anchors[i + 1]
const href1 = a1.getAttribute('href') || ''
const text2 = a2.textContent?.trim() || ''
const href2 = a2.getAttribute('href') || ''
// first link is base watch URL, second has sm ID text
if (
/^(https?:)?\/\/(www\.)?nicovideo\.jp\/watch\/?$/.test(href1) &&
/^sm\d+$/.test(text2) &&
href2.includes(`/watch/${text2}`)
) {
const newHref = href2.replace(/^(https?:)?/, '//')
const newA = document.createElement('a')
newA.href = newHref
newA.target = '_blank'
newA.textContent = text2
a1.replaceWith(newA)
a2.remove()
console.log(`Merged Nico link: ${text2}`)
// skip the next element
i++
}
}
}
const processLinks = () => {
try {
processAcgLinks()
processDescLinks()
normalizeNicoDescLinks()
} catch (error) {
console.error('处理链接时遇到 Error:', error)
}
}
const setupObservers = () => {
if (observer) {
observer.disconnect()
}
observer = new MutationObserver(processLinks)
observer.observe(document.documentElement, {
childList: true,
subtree: true,
})
console.log('已设置 Observer')
}
const entry = () => {
console.log('视频链接增强已启用')
processLinks()
setupObservers()
}
export const component = defineComponentMetadata({
name: 'activeVideoLinks',
displayName: '视频链接增强',
tags: [componentsTags.utils],
entry,
reload: entry,
unload: () => {
if (observer) {
observer.disconnect()
observer = null
}
},
urlInclude: videoUrls,
author: {
name: 'Alan Ye',
link: 'https://github.com/at-wr',
},
})