您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bionic Reading User Script ⌘ + B
当前为
// ==UserScript== // @name Bionic Reading Neo // @namespace http://tampermonkey.net/ // @version 0.1.0 // @description Bionic Reading User Script ⌘ + B // @author RoCry // @match *://*/* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAIMSURBVHgB7ZfvtcFAEMU377zvlEAHOkAFqAAdUAEqoAM6QAV0QAeogA68/W1M3ubPShbn5It7zpDE7OzNzJ3ZI7hrqBLxo0rGl8AvH+fzWbXb7VznWq2mqtWqajQaqt/vm/t3ESBCCNTrdeWLwWCg5vO5IQVWq5XTF7IQF9+PEAAEPRwOYbAgyNwEHI9HdbvdDOnJZPKfPQicTidaMWY6yF07RcZ90kdMv/n90c4mlguaaBRP/JwEptNpKgB+3W435dtqtQoREOAfrfEhAK7XayobvJEPgd1uZ3yJ5d2G1Dep/qyaP4N+e7Nmu92+NgcQrQ2E5wt5CS8CqHg4HJpvOxCq9gUxKpVKOIiyQE/v9/vYM2kle/P1eu09kMggZta5RJhniBQR2VAFRahnQX4XFDHUb3dLHgEIs7k9B5wl0P2uOp1OdE/qL5eL2mw2kQj51gTMtehgPB47JyFGB+g2jMrmJICyGZtJsBEHF8EEkGg2m+ZaDqwsAsRk8xhcJXANInuQ2DYajZ6WgDHM76y18dIcII1JJGdDEpLR2WwWe/4SATv9Ars9XaB8rLXb25sAAXq9Xup5kWmIPnSpzDCL4NIArSKnlhjPlKMlJUbeHJDDbLFYhK3rIuBjOrVhsIKDCIFDAjJvE5DNfQhIFiDiTYCFut5m4+RmRQmA5XJpYgWPhaXh+8fkS6B0An855cAlu9FutwAAAABJRU5ErkJggg== // @exclude /\.(js|java|c|cpp|h|py|css|less|scss|json|yaml|yml|xml)(?:\?.+)$/ // @license MIT // @run-at document-end // @grant GM_getValue // @grant GM_setValue // ==/UserScript== /* To change config, open browser console and run: 1. Get current config: > GM_getValue('config') 2. Set new config (example): > const newConfig = GM_getValue('config') > newConfig.scale = 0.7 // change any option you want > GM_setValue('config', newConfig) 3. Refresh the page to apply changes Available config options are listed below in defaultConfig */ const defaultConfig = { // Whether to automatically apply bionic reading when page loads autoBionic: false, // Whether to skip processing text inside <a> tags that contain URLs skipLinks: true, // The ratio of characters to bold at the start of each word (0.0 to 1.0) scale: 0.5, // Maximum number of characters to bold in any word (null for no limit) maxBionicLength: null, // Opacity of the bold text (0.0 to 1.0) opacity: 1, // Number of words to skip between processed words (0 for no skipping) // Higher values create a "saccade" effect, mimicking natural eye movement saccade: 0, // Whether to use special Unicode characters instead of bold text symbolMode: false, // Whether to skip processing words in the excludeWords list skipWords: true, // List of common words to skip when skipWords is true excludeWords: ['is','and','as','if','the','of','to','be','for','this'], // Minimum word length to apply bionic reading (shorter words are skipped) minWordLength: 3, // Minimum ratio of ASCII characters required to consider content as English // (0.0 to 1.0) - Higher values mean stricter English detection minAsciiRatio: 0.9, // Number of characters to analyze for language detection charsToCheck: 300 }; let config = defaultConfig; try { config = (_=>{ const _config = GM_getValue('config'); if(!_config) return defaultConfig; for(let key in defaultConfig){ if(_config[key] === undefined) _config[key] = defaultConfig[key]; } return _config; })(); GM_setValue('config',config); } catch(e) { console.log('Failed to read default config') } let isBionic = false; let body = document.body; const styleEl = document.createElement('style'); styleEl.textContent = `bbb{font-weight:bold;opacity:${config.opacity}}html[data-site="greasyfork"] a bionic{pointer-events:none}`; document.documentElement.setAttribute('data-site',location.hostname.replace(/\.\w+$|www\./ig,'')) const excludeNodeNames = [ 'script','style','xmp', 'input','textarea','select', 'pre','code', 'h1','h2', 'b','strong', 'svg','embed', 'img','audio','video', 'canvas', ]; const excludeClasses = [ 'highlight', 'katex', 'editor', ] const excludeClassesRegexi = new RegExp(excludeClasses.join('|'),'i'); const linkRegex = /^https?:\/\//; function isEnglishContent() { try { const title = document.title || ''; const firstParagraphs = Array.from(document.getElementsByTagName('p')) .slice(0, 3) .map(p => p.textContent) .join(' '); const textToAnalyze = (title + ' ' + firstParagraphs) .slice(0, config.charsToCheck) .replace(/\s+/g, ' ') .trim(); if (!textToAnalyze) return true; const asciiChars = textToAnalyze.replace(/[\s\.,\-_'"!?()]/g, '') .split('') .filter(char => char.charCodeAt(0) <= 127).length; const totalChars = textToAnalyze.replace(/[\s\.,\-_'"!?()]/g, '').length; if (totalChars === 0) return true; const asciiRatio = asciiChars / totalChars; console.log('🈂️ ASCII Ratio:', asciiRatio.toFixed(2)); return asciiRatio >= config.minAsciiRatio; } catch (e) { console.error('Error checking content language:', e); return true; } } const gather = el=>{ let textEls = []; el.childNodes.forEach(el=>{ if(el.isEnB) return; if(el.originEl) return; if(el.nodeType === 3){ textEls.push(el); }else if(el.childNodes){ const nodeName = el.nodeName.toLowerCase(); if(excludeNodeNames.includes(nodeName)) return; if(config.skipLinks){ if(nodeName === 'a'){ if(linkRegex.test(el.textContent)) return; } } if(el.getAttribute){ if(el.getAttribute('class') && excludeClassesRegexi.test(el.getAttribute('class'))) return; if(el.getAttribute('contentEditable') === 'true') return; } textEls = textEls.concat(gather(el)) } }) return textEls; }; const engRegex = /[a-zA-Z][a-z]+/; const engRegexg = new RegExp(engRegex,'g'); const getHalfLength = word=>{ if (word.length < config.minWordLength) return 0; let halfLength; if(/ing$/.test(word)){ halfLength = word.length - 3; }else if(word.length<5){ halfLength = Math.floor(word.length * config.scale); }else{ halfLength = Math.ceil(word.length * config.scale); } if(config.maxBionicLength){ halfLength = Math.min(halfLength, config.maxBionicLength) } return halfLength; } let count = 0; const saccadeRound = config.saccade + 1; const saccadeCounter = _=>{ return ++count % saccadeRound === 0; }; const replaceTextByEl = el=>{ const text = el.data; if(!engRegex.test(text))return; if(!el.replaceEl){ const spanEl = document.createElement('bionic'); spanEl.isEnB = true; // Split text into parts and create DOM elements instead of using innerHTML const parts = text.split(engRegexg); const matches = text.match(engRegexg) || []; let currentIndex = 0; // Add first text part if exists if (parts[0]) { spanEl.appendChild(document.createTextNode(parts[0])); currentIndex++; } // Process each matched word matches.forEach((word, i) => { if(word.length < config.minWordLength || (config.skipWords && config.excludeWords.includes(word)) || (config.saccade && !saccadeCounter())) { spanEl.appendChild(document.createTextNode(word)); } else { const halfLength = getHalfLength(word); if (halfLength === 0) { spanEl.appendChild(document.createTextNode(word)); } else { const boldPart = document.createElement('bbb'); boldPart.textContent = word.substr(0, halfLength); spanEl.appendChild(boldPart); spanEl.appendChild(document.createTextNode(word.substr(halfLength))); } } // Add text part after the word if exists if (parts[currentIndex]) { spanEl.appendChild(document.createTextNode(parts[currentIndex])); } currentIndex++; }); spanEl.originEl = el; el.replaceEl = spanEl; } el.after(el.replaceEl); el.remove(); }; const replaceTextSymbolModeByEl = el=>{ const text = el.data; if(!engRegex.test(text))return; // For symbol mode, we can still use textContent since we're not creating HTML el.data = text.replace(engRegexg,word=>{ if(word.length < config.minWordLength) return word; if(config.skipWords && config.excludeWords.includes(word)) return word; if(config.saccade && !saccadeCounter()) return word; const halfLength = getHalfLength(word); if (halfLength === 0) return word; const a = word.substr(0,halfLength). replace(/[a-z]/g,w=>String.fromCharCode(55349,w.charCodeAt(0)+56717)). replace(/[A-Z]/g,w=>String.fromCharCode(55349,w.charCodeAt(0)+56723)); const b = word.substr(halfLength). replace(/[a-z]/g,w=> String.fromCharCode(55349,w.charCodeAt(0)+56665)). replace(/[A-Z]/g,w=> String.fromCharCode(55349,w.charCodeAt(0)+56671)); return a + b; }) } const bionic = _=>{ if (!isEnglishContent()) { console.log('🈂️ Non-English content detected, skipping bionic reading'); return; } const textEls = gather(body); isBionic = true; count = 0; let replaceFunc = config.symbolMode ? replaceTextSymbolModeByEl : replaceTextByEl; textEls.forEach(replaceFunc); document.head.appendChild(styleEl); } const lazy = (func,ms = 15)=> { return _=>{ clearTimeout(func.T) func.T = setTimeout(func,ms) } }; const listenerFunc = lazy(_=>{ if(!isBionic) return; bionic(); }); if(window.MutationObserver){ (new MutationObserver(listenerFunc)).observe(body,{ childList: true, subtree: true, attributes: true, }); }else{ const {open,send} = XMLHttpRequest.prototype; XMLHttpRequest.prototype.open = function(){ this.addEventListener('load',listenerFunc); return open.apply(this,arguments); }; document.addEventListener('DOMContentLoaded',listenerFunc); document.addEventListener('DOMNodeInserted',listenerFunc); } if(config.autoBionic){ window.addEventListener('load',bionic); } const revoke = _=>{ const els = [...document.querySelectorAll('bionic')]; els.forEach(el=>{ const {originEl} = el; if(!originEl) return; el.after(originEl); el.remove(); }) isBionic = false; }; document.addEventListener('keydown',e=>{ const { ctrlKey , metaKey, key } = e; if( ctrlKey || metaKey ){ if(key === 'b'){ if(isBionic){ revoke(); }else{ bionic(); } } } })