|
// ==UserScript== |
|
// @name 微信文章 Obsidian Clipper 辅助 |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.3 |
|
// @description 优化微信公众号文章图片和代码块,方便 Obsidian Clipper 剪藏 |
|
// @author Jack Drogon |
|
// @match *://mp.weixin.qq.com/* |
|
// @run-at document-idle |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
function normalizeImageUrl(value) { |
|
if (!value) return ''; |
|
|
|
const rawUrl = value.trim(); |
|
|
|
if ( |
|
rawUrl.startsWith('data:') || |
|
rawUrl.startsWith('blob:') || |
|
rawUrl.startsWith('about:') |
|
) { |
|
return ''; |
|
} |
|
|
|
try { |
|
const url = new URL(rawUrl, window.location.href); |
|
|
|
if (url.hostname === 'mmbiz.qpic.cn') { |
|
url.searchParams.delete('tp'); |
|
url.searchParams.delete('wxfrom'); |
|
url.searchParams.delete('wx_lazy'); |
|
|
|
if (/^#imgIndex=\d+$/i.test(url.hash)) { |
|
url.hash = ''; |
|
} |
|
} |
|
|
|
return url.href; |
|
} catch (error) { |
|
return rawUrl; |
|
} |
|
} |
|
|
|
function isPlaceholderSrc(value) { |
|
const src = (value || '').trim().toLowerCase(); |
|
|
|
return ( |
|
!src || |
|
src === 'about:blank' || |
|
src.startsWith('data:image/') || |
|
src.includes('pic_blank') || |
|
src.includes('placeholder') || |
|
src.includes('transparent') |
|
); |
|
} |
|
|
|
function getRealImageSrc(img) { |
|
return ( |
|
img.getAttribute('data-src') || |
|
img.getAttribute('data-original-src') || |
|
img.getAttribute('data-original') || |
|
img.getAttribute('data-lazy-src') || |
|
img.getAttribute('data-actualsrc') || |
|
img.getAttribute('data-url') || |
|
img.getAttribute('data-image') || |
|
img.getAttribute('data-img-src') || |
|
'' |
|
); |
|
} |
|
|
|
function fixImage(img, index) { |
|
const realSrc = normalizeImageUrl(getRealImageSrc(img)); |
|
const currentSrc = normalizeImageUrl(img.getAttribute('src')); |
|
|
|
if (!realSrc) return false; |
|
|
|
const shouldReplace = |
|
isPlaceholderSrc(img.getAttribute('src')) || |
|
currentSrc !== realSrc; |
|
|
|
if (shouldReplace) { |
|
img.setAttribute('src', realSrc); |
|
} |
|
|
|
img.setAttribute('data-obsidian-clipper-src', realSrc); |
|
|
|
if (!img.getAttribute('alt')) { |
|
img.setAttribute('alt', `wechat-image-${index}`); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
function fixArticleImages() { |
|
document.querySelectorAll('#js_content img').forEach((img, index) => { |
|
fixImage(img, index); |
|
}); |
|
} |
|
|
|
function normalizeWeChatSnippetCodeBlocks() { |
|
const blocks = document.querySelectorAll('#js_content .code-snippet__fix'); |
|
|
|
blocks.forEach(block => { |
|
if (block.getAttribute('data-obsidian-code-host') === 'true') return; |
|
if (block.closest('[data-obsidian-code-host="true"]')) return; |
|
|
|
const sourcePre = block.querySelector('pre'); |
|
if (!sourcePre) return; |
|
|
|
const language = normalizeCodeLanguage( |
|
sourcePre.getAttribute('data-lang') || |
|
sourcePre.getAttribute('data-language') || |
|
detectLanguageFromClass(sourcePre) || |
|
'' |
|
); |
|
|
|
const codeText = extractSnippetCodeText(sourcePre); |
|
if (!codeText.trim()) return; |
|
|
|
replaceWithShadowCodeHost(block, codeText, language); |
|
}); |
|
} |
|
|
|
function normalizeMdniceCodeBlocks() { |
|
const blocks = Array.from(document.querySelectorAll('#js_content pre')).filter(pre => { |
|
if (pre.getAttribute('data-obsidian-code-host') === 'true') return false; |
|
if (pre.closest('[data-obsidian-code-host="true"]')) return false; |
|
|
|
const code = pre.querySelector('code'); |
|
const tool = pre.getAttribute('data-tool') || ''; |
|
|
|
return ( |
|
code && |
|
( |
|
tool.includes('mdnice') || |
|
tool.includes('mdnice编辑器') || |
|
code.querySelector('br') |
|
) |
|
); |
|
}); |
|
|
|
blocks.forEach(pre => { |
|
const code = pre.querySelector('code'); |
|
if (!code) return; |
|
|
|
const language = normalizeCodeLanguage( |
|
pre.getAttribute('data-lang') || |
|
code.getAttribute('data-lang') || |
|
code.getAttribute('data-language') || |
|
detectLanguageFromClass(pre) || |
|
detectLanguageFromClass(code) || |
|
detectLanguageFromCode(code.innerText || code.textContent || '') |
|
); |
|
|
|
const codeText = normalizeCodeText(extractTextWithBreaks(code)); |
|
if (!codeText.trim()) return; |
|
|
|
replaceWithShadowCodeHost(pre, codeText, language); |
|
}); |
|
} |
|
|
|
function replaceWithShadowCodeHost(originalBlock, codeText, language) { |
|
const host = document.createElement('section'); |
|
const shadow = host.attachShadow({ mode: 'open' }); |
|
const displayClone = originalBlock.cloneNode(true); |
|
const clipperBlock = createClipperCodeBlock(codeText, language); |
|
|
|
host.setAttribute('data-obsidian-code-host', 'true'); |
|
host.style.display = 'block'; |
|
|
|
displayClone.removeAttribute('data-obsidian-code-host'); |
|
displayClone.setAttribute('data-obsidian-code-display', 'true'); |
|
|
|
shadow.appendChild(createShadowStyle()); |
|
shadow.appendChild(displayClone); |
|
|
|
// 普通 DOM 中只保留给 Clipper 读取的标准代码块。 |
|
// Shadow DOM 没有 slot,所以这个节点不会显示在页面上。 |
|
host.appendChild(clipperBlock); |
|
|
|
originalBlock.replaceWith(host); |
|
} |
|
|
|
function createShadowStyle() { |
|
const style = document.createElement('style'); |
|
|
|
style.textContent = ` |
|
:host { |
|
display: block; |
|
} |
|
|
|
pre { |
|
max-width: 100%; |
|
box-sizing: border-box; |
|
} |
|
|
|
.code-snippet__fix { |
|
word-wrap: break-word !important; |
|
font-size: 14px; |
|
margin: 10px 0; |
|
color: #333; |
|
position: relative; |
|
background-color: rgba(0, 0, 0, 0.03); |
|
border: 1px solid #f0f0f0; |
|
border-radius: 2px; |
|
display: flex !important; |
|
line-height: 26px; |
|
overflow: hidden; |
|
} |
|
|
|
.code-snippet__fix .code-snippet__line-index { |
|
counter-reset: line; |
|
flex-shrink: 0; |
|
height: 100%; |
|
margin: 0; |
|
padding: 1em 0.75em; |
|
list-style-type: none; |
|
background: rgba(0, 0, 0, 0.02); |
|
} |
|
|
|
.code-snippet__fix .code-snippet__line-index li { |
|
list-style-type: none; |
|
text-align: right; |
|
min-width: 1.5em; |
|
height: 26px; |
|
line-height: 26px; |
|
} |
|
|
|
.code-snippet__fix .code-snippet__line-index li::before { |
|
counter-increment: line; |
|
content: counter(line); |
|
color: rgba(0, 0, 0, 0.35); |
|
font-size: 12px; |
|
} |
|
|
|
.code-snippet__fix pre { |
|
flex: 1; |
|
margin: 0; |
|
padding: 1em; |
|
overflow-x: auto; |
|
white-space: normal; |
|
-webkit-overflow-scrolling: touch; |
|
} |
|
|
|
.code-snippet__fix code { |
|
display: flex !important; |
|
position: relative; |
|
text-align: left; |
|
white-space: pre !important; |
|
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; |
|
font-size: 14px; |
|
line-height: 26px; |
|
min-height: 26px; |
|
} |
|
|
|
.code-snippet__fix code span { |
|
white-space: pre !important; |
|
} |
|
|
|
.code-snippet_outer { |
|
white-space: pre !important; |
|
} |
|
|
|
.code-snippet__keyword { |
|
color: #a626a4; |
|
} |
|
|
|
.code-snippet__string { |
|
color: #50a14f; |
|
} |
|
|
|
.code-snippet__number { |
|
color: #986801; |
|
} |
|
|
|
.code-snippet__comment { |
|
color: #a0a1a7; |
|
font-style: italic; |
|
} |
|
|
|
.code-snippet__built_in { |
|
color: #4078f2; |
|
} |
|
|
|
.code-snippet__function { |
|
color: #4078f2; |
|
} |
|
|
|
.code-snippet__literal { |
|
color: #0184bc; |
|
} |
|
|
|
.code-snippet__params { |
|
color: #383a42; |
|
} |
|
`; |
|
|
|
return style; |
|
} |
|
|
|
function createClipperCodeBlock(codeText, language) { |
|
const pre = document.createElement('pre'); |
|
const code = document.createElement('code'); |
|
|
|
pre.setAttribute('data-obsidian-code-for-clipper', 'true'); |
|
pre.style.whiteSpace = 'pre'; |
|
|
|
code.style.whiteSpace = 'pre'; |
|
|
|
if (language) { |
|
code.className = `language-${language}`; |
|
code.setAttribute('data-lang', language); |
|
} |
|
|
|
code.textContent = normalizeCodeText(codeText); |
|
pre.appendChild(code); |
|
|
|
return pre; |
|
} |
|
|
|
function extractSnippetCodeText(sourcePre) { |
|
const codeNodes = Array.from(sourcePre.querySelectorAll('code')); |
|
|
|
if (codeNodes.length > 1) { |
|
return codeNodes |
|
.map(code => { |
|
if (code.querySelector('br') && !code.textContent.trim()) { |
|
return ''; |
|
} |
|
|
|
return normalizeCodeLine(code.textContent || ''); |
|
}) |
|
.join('\n') |
|
.replace(/\n+$/g, ''); |
|
} |
|
|
|
const sourceCode = sourcePre.querySelector('code') || sourcePre; |
|
return extractTextWithBreaks(sourceCode); |
|
} |
|
|
|
function extractTextWithBreaks(root) { |
|
function walk(node) { |
|
if (node.nodeType === Node.TEXT_NODE) { |
|
return node.textContent || ''; |
|
} |
|
|
|
if (node.nodeType !== Node.ELEMENT_NODE) { |
|
return ''; |
|
} |
|
|
|
const element = node; |
|
|
|
if (element.tagName === 'BR') { |
|
return '\n'; |
|
} |
|
|
|
if ( |
|
element.tagName === 'SCRIPT' || |
|
element.tagName === 'STYLE' || |
|
element.tagName === 'BUTTON' |
|
) { |
|
return ''; |
|
} |
|
|
|
return Array.from(element.childNodes).map(walk).join(''); |
|
} |
|
|
|
return walk(root); |
|
} |
|
|
|
function normalizeCodeText(text) { |
|
return (text || '') |
|
.replace(/\u00a0/g, ' ') |
|
.replace(/\r\n/g, '\n') |
|
.replace(/\r/g, '\n') |
|
.replace(/[ \t]+\n/g, '\n') |
|
.replace(/^\n+|\n+$/g, ''); |
|
} |
|
|
|
function normalizeCodeLine(line) { |
|
return (line || '') |
|
.replace(/\u00a0/g, ' ') |
|
.replace(/\r/g, '') |
|
.replace(/\n/g, ''); |
|
} |
|
|
|
function normalizeCodeLanguage(value) { |
|
return (value || '') |
|
.trim() |
|
.toLowerCase() |
|
.replace(/^language-/, '') |
|
.replace(/^lang-/, '') |
|
.replace(/^brush-/, '') |
|
.replace(/[^a-z0-9_+-]/g, ''); |
|
} |
|
|
|
function detectLanguageFromClass(element) { |
|
if (!element) return ''; |
|
|
|
const classText = [ |
|
element.className || '', |
|
element.closest('.code-snippet__fix')?.className || '' |
|
].join(' '); |
|
|
|
const languageMatch = classText.match(/(?:language|lang|brush)-([a-z0-9_+-]+)/i); |
|
if (languageMatch) return languageMatch[1]; |
|
|
|
const wechatMatch = classText.match(/code-snippet__([a-z0-9_+-]+)/i); |
|
if (wechatMatch && wechatMatch[1] !== 'fix') return wechatMatch[1]; |
|
|
|
return ''; |
|
} |
|
|
|
function detectLanguageFromCode(text) { |
|
const source = text || ''; |
|
|
|
if ( |
|
source.includes('import torch') || |
|
source.includes('import numpy') || |
|
source.includes('def ') || |
|
source.includes('class ') || |
|
source.includes('self.') |
|
) { |
|
return 'python'; |
|
} |
|
|
|
if ( |
|
source.includes('const ') || |
|
source.includes('let ') || |
|
source.includes('function ') || |
|
source.includes('=>') |
|
) { |
|
return 'javascript'; |
|
} |
|
|
|
if ( |
|
source.includes('interface ') || |
|
source.includes(': string') || |
|
source.includes(': number') |
|
) { |
|
return 'typescript'; |
|
} |
|
|
|
if ( |
|
source.includes('package main') || |
|
source.includes('func ') |
|
) { |
|
return 'go'; |
|
} |
|
|
|
return ''; |
|
} |
|
|
|
function normalizeCodeBlocks() { |
|
normalizeWeChatSnippetCodeBlocks(); |
|
normalizeMdniceCodeBlocks(); |
|
} |
|
|
|
function run() { |
|
fixArticleImages(); |
|
normalizeCodeBlocks(); |
|
} |
|
|
|
run(); |
|
setTimeout(run, 1000); |
|
setTimeout(run, 3000); |
|
setTimeout(run, 5000); |
|
})(); |