Bionic Reading / ⌘ + B

Bionic Reading User Script Ctrl + B / ⌘ + B

  1. // ==UserScript==
  2. // @name Bionic Reading / ⌘ + B
  3. // @name:zh 英文前部加粗 / ⌘ + B
  4. // @namespace https://github.com/itorr/bionic-reading.user.js
  5. // @version 0.8.4
  6. // @description Bionic Reading User Script Ctrl + B / ⌘ + B
  7. // @description:zh 网页英文前部加粗脚本 Ctrl + B / ⌘ + B 开启关闭
  8. // @icon data:image/vnd.microsoft.icon;base64,AAABAAEAICACAAEAAQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABvb28Ab29vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////////////8AAfH/AABx/wAAMf8AADH/D/gR/w/8Ef8P/BH/D/wR/w/8Ef8P+DH/AAAx/wAAcf8AAPD/AABwfw/wMA8P+DEPD/gxjw/4P/8P8D//AAA//wAAf/8AAP//AAP////////////////////////////8=
  9. // @author itorr
  10. // @match *://*/*
  11. // @exclude /\.(js|java|c|cpp|h|py|css|less|scss|json|yaml|yml|xml)(?:\?.+)$/
  12. // @license MIT
  13. // @run-at document-end
  14. // @supportURL https://github.com/itorr/bionic-reading.user.js/issues
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // ==/UserScript==
  18.  
  19.  
  20. const defaultConfig = {
  21. autoBionic: true,
  22. skipLinks: false,
  23. skipWords: false,
  24. scale: 0.5,
  25. maxBionicLength: null,
  26. opacity: 1,
  27. saccade: 0, // 0 - ~
  28. symbolMode: false,
  29. excludeWords:['is','and','as','if','the','of','to','be','for','this'],
  30. };
  31.  
  32. let config = defaultConfig;
  33. try{
  34. config = (_=>{
  35. const _config = GM_getValue('config');
  36. if(!_config) return defaultConfig;
  37. for(let key in defaultConfig){
  38. if(_config[key] === undefined) _config[key] = defaultConfig[key];
  39. }
  40. return _config;
  41. })();
  42. GM_setValue('config',config);
  43. }catch(e){
  44. console.log('读取默认配置失败')
  45. }
  46.  
  47. console.log(JSON.stringify(config,0,2))
  48.  
  49. let isBionic = false;
  50.  
  51. const enCodeHTML = s=> s.replace(/[\u00A0-\u9999<>\&]/g,w=>'&#'+w.charCodeAt(0)+';');
  52.  
  53. let body = document.body;
  54.  
  55. if(/weibo/.test(location.hostname)){
  56. const wbMainEl = document.querySelector('.WB_main');
  57. if(wbMainEl) body = wbMainEl;
  58.  
  59. // 修复旧版微博自定义样式失效 bug
  60. const customStyleEl = document.querySelector('#custom_style');
  61. if(customStyleEl) customStyleEl.removeAttribute('id');
  62. }
  63.  
  64. const styleEl = document.createElement('style');
  65. styleEl.innerHTML = `
  66. bbb{
  67. font-weight:bold;
  68. opacity: ${config.opacity};
  69. }
  70. html[data-site="greasyfork"] a bionic{
  71. pointer-events: none;
  72. }
  73. `;
  74.  
  75. document.documentElement.setAttribute('data-site',location.hostname.replace(/\.\w+$|www\./ig,''))
  76.  
  77. const excludeNodeNames = [
  78. 'script','style','xmp',
  79. 'input','textarea','select',
  80. 'pre','code',
  81. 'h1','h2', // 'h3','h4',
  82. 'b','strong',
  83. 'svg','embed',
  84. 'img','audio','video',
  85. 'canvas',
  86. ];
  87.  
  88. const excludeClasses = [
  89. 'highlight',
  90. 'katex',
  91. 'editor',
  92. ]
  93. const excludeClassesRegexi = new RegExp(excludeClasses.join('|'),'i');
  94. const linkRegex = /^https?:\/\//;
  95. const gather = el=>{
  96. let textEls = [];
  97. el.childNodes.forEach(el=>{
  98. if(el.isEnB) return;
  99. if(el.originEl) return;
  100.  
  101. if(el.nodeType === 3){
  102. textEls.push(el);
  103. }else if(el.childNodes){
  104. const nodeName = el.nodeName.toLowerCase();
  105. if(excludeNodeNames.includes(nodeName)) return;
  106. if(config.skipLinks){
  107. if(nodeName === 'a'){
  108. if(linkRegex.test(el.textContent)) return;
  109. }
  110. }
  111. if(el.getAttribute){
  112. if(el.getAttribute('class') && excludeClassesRegexi.test(el.getAttribute('class'))) return;
  113.  
  114. // 跳过所有可编辑元素
  115. if(el.getAttribute('contentEditable') === 'true') return;
  116. }
  117.  
  118. textEls = textEls.concat(gather(el))
  119. }
  120. })
  121. return textEls;
  122. };
  123.  
  124. const engRegex = /[a-zA-Z][a-z]+/;
  125. const engRegexg = new RegExp(engRegex,'g');
  126. const getHalfLength = word=>{
  127.  
  128. let halfLength;
  129. if(/ing$/.test(word)){
  130. halfLength = word.length - 3;
  131. }else if(word.length<5){
  132. halfLength = Math.floor(word.length * config.scale);
  133. }else{
  134. halfLength = Math.ceil(word.length * config.scale);
  135. }
  136.  
  137. if(config.maxBionicLength){
  138. halfLength = Math.min(halfLength, config.maxBionicLength)
  139. }
  140. return halfLength;
  141. }
  142.  
  143.  
  144. let count = 0;
  145. const saccadeRound = config.saccade + 1;
  146. const saccadeCounter = _=>{
  147. return ++count % saccadeRound === 0;
  148. };
  149. const replaceTextByEl = el=>{
  150. const text = el.data;
  151. if(!engRegex.test(text))return;
  152.  
  153. if(!el.replaceEl){
  154. const spanEl = document.createElement('bionic');
  155. spanEl.isEnB = true;
  156. spanEl.innerHTML = enCodeHTML(text).replace(engRegexg,word=>{
  157. if(config.skipWords && config.excludeWords.includes(word)) return word;
  158. if(config.saccade && !saccadeCounter()) return word;
  159.  
  160. const halfLength = getHalfLength(word);
  161. return '<bbb>'+word.substr(0,halfLength)+'</bbb>'+word.substr(halfLength)
  162. })
  163. spanEl.originEl = el;
  164. el.replaceEl = spanEl;
  165. }
  166.  
  167. el.after(el.replaceEl);
  168. el.remove();
  169. };
  170.  
  171. const replaceTextSymbolModeByEl = el=>{
  172. el.data = el.data.replace(engRegexg,word=>{
  173. if(config.skipWords && config.excludeWords.includes(word)) return word;
  174. if(config.saccade && !saccadeCounter()) return word;
  175.  
  176. const halfLength = getHalfLength(word);
  177. const a = word.substr(0,halfLength).
  178. replace(/[a-z]/g,w=>String.fromCharCode(55349,w.charCodeAt(0)+56717)).
  179. replace(/[A-Z]/g,w=>String.fromCharCode(55349,w.charCodeAt(0)+56723));
  180. const b = word.substr(halfLength).
  181. replace(/[a-z]/g,w=> String.fromCharCode(55349,w.charCodeAt(0)+56665)).
  182. replace(/[A-Z]/g,w=> String.fromCharCode(55349,w.charCodeAt(0)+56671));
  183. return a + b;
  184. })
  185. }
  186.  
  187. const bionic = _=>{
  188. const textEls = gather(body);
  189.  
  190. isBionic = true;
  191. count = 0;
  192.  
  193. let replaceFunc = config.symbolMode ? replaceTextSymbolModeByEl : replaceTextByEl;
  194. textEls.forEach(replaceFunc);
  195.  
  196. document.head.appendChild(styleEl);
  197. }
  198.  
  199. const lazy = (func,ms = 15)=> {
  200. return _=>{
  201. clearTimeout(func.T)
  202. func.T = setTimeout(func,ms)
  203. }
  204. };
  205.  
  206. const listenerFunc = lazy(_=>{
  207. if(!isBionic) return;
  208.  
  209. bionic();
  210. });
  211.  
  212. if(window.MutationObserver){
  213. (new MutationObserver(listenerFunc)).observe(body,{
  214. childList: true,
  215. subtree: true,
  216. attributes: true,
  217. });
  218. }else{
  219. const {open,send} = XMLHttpRequest.prototype;
  220. XMLHttpRequest.prototype.open = function(){
  221. this.addEventListener('load',listenerFunc);
  222. return open.apply(this,arguments);
  223. };
  224. document.addEventListener('DOMContentLoaded',listenerFunc);
  225. document.addEventListener('DOMNodeInserted',listenerFunc);
  226. }
  227.  
  228. if(config.autoBionic){ // auto Bionic
  229. window.addEventListener('load',bionic);
  230. }
  231. // document.addEventListener('click',listenerFunc);
  232.  
  233.  
  234. const revoke = _=>{
  235. const els = [...document.querySelectorAll('bionic')];
  236.  
  237. els.forEach(el=>{
  238. const {originEl} = el;
  239. if(!originEl) return;
  240.  
  241. el.after(originEl);
  242. el.remove();
  243. })
  244.  
  245. isBionic = false;
  246. };
  247. // document.addEventListener('mousedown',revoke);
  248.  
  249. const redo = _=>{
  250. const textEls = gather(body);
  251.  
  252. textEls.forEach(el=>{
  253. const { replaceEl } = el;
  254.  
  255. if(!replaceEl) return;
  256.  
  257.  
  258. el.after(replaceEl);
  259. el.remove();
  260. })
  261.  
  262. isBionic = false;
  263. };
  264.  
  265. document.addEventListener('keydown',e=>{
  266. const { ctrlKey , metaKey, key } = e;
  267.  
  268. if( ctrlKey || metaKey ){
  269. if(key === 'b'){
  270. if(isBionic){
  271. revoke();
  272. }else{
  273. bionic();
  274. }
  275. }
  276. }
  277. })
  278.  
  279.  
  280. // let id = base.registerMenuCommand ('Setting', function(){
  281. // // 配置相关
  282. // }, 's');
  283.  
  284.  
  285. // document.addEventListener('mouseup',redo);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址