Tencent Translator Enhancer

It brings back-and-forth translation to Tencent Translator (腾讯翻译君).

  1. // ==UserScript==
  2. // @name Tencent Translator Enhancer
  3. // @name:ja Tencent Translator Enhancer
  4. // @name:zh-CN Tencent Translator Enhancer
  5. // @description It brings back-and-forth translation to Tencent Translator (腾讯翻译君).
  6. // @description:ja 騰訊翻訳君(腾讯翻译君)に往復翻訳などの機能を追加します。
  7. // @description:zh-CN 在腾讯翻译君中添加往返翻译等功能。
  8. // @namespace knoa.jp
  9. // @include https://fanyi.qq.com/
  10. // @version 1.3.0
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. const SCRIPTID = 'TencentTranslatorEnhancer';
  16. const SCRIPTNAME = 'Tencent Translator Enhancer';
  17. const DEBUG = false;/*
  18. [update]
  19. Focus on the textarea when the tab got focused. And minor fix.
  20.  
  21. [bug]
  22.  
  23. [todo]
  24. ウィンドウフォーカスでテキストエリアにフォーカスだよね
  25. #...を使って外部からテキストの受け渡しができるAPIとか?
  26. しかし現行の自分のChromeアプリに渡せる手段がないような?
  27.  
  28. [possible]
  29.  
  30. [research]
  31. 効かなくなったら data-selector が付いてるか確認
  32.  
  33. [memo]
  34. */
  35. if(window === top && console.time) console.time(SCRIPTID);
  36. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  37. const LANGUAGES = [/^en/, /^zh/, /^ja/];/* [0] がデフォルト */
  38. const LABELS = {
  39. '自动识别': ['Auto-Detect', '自动识别', '自動認識', ],
  40. '自动检测': ['Auto-Detect', '自动检测', '自動認識', ],
  41. '检测到中文': ['Chinese detected', '检测到中文', '中国語 検出', ],
  42. '检测到英语': ['English detected', '检测到英语', '英語 検出', ],
  43. '检测到日语': ['Japanese detected', '检测到日语', '日本語 検出', ],
  44. '检测到韩语': ['Korean detected', '检测到韩语', '韓国語 検出', ],
  45. '检测到法语': ['French detected', '检测到法语', 'フランス語 検出', ],
  46. '检测到西班牙语': ['Spanish detected', '检测到西班牙语', 'スペイン語 検出', ],
  47. '检测到意大利语': ['Italian detected', '检测到意大利语', 'イタリア語 検出', ],
  48. '检测到德语': ['German detected', '检测到德语', 'ドイツ語 検出', ],
  49. '检测到土耳其语': ['Turkish detected', '检测到土耳其语', 'トルコ語 検出', ],
  50. '检测到俄语': ['Russian detected', '检测到俄语', 'ロシア語 検出', ],
  51. '检测到葡萄牙语': ['Portuguese detected', '检测到葡萄牙语', 'ポルトガル語 検出', ],
  52. '检测到越南语': ['Vietnamese detected', '检测到越南语', 'ベトナム語 検出', ],
  53. '检测到印尼语': ['Indonesian detected', '检测到印尼语', 'インドネシア語 検出',],
  54. '检测到泰语': ['Thai detected', '检测到泰语', 'タイ語 検出', ],
  55. '检测到马来西亚语': ['Malaysian detected', '检测到马来西亚语', 'マレーシア語 検出', ],
  56. '检测到阿拉伯语': ['Arabic detected', '检测到阿拉伯语', 'アラビア語 検出', ],
  57. '检测到印地语': ['Hindi detected', '检测到印地语', 'ヒンディー語 検出', ],
  58. '中文': ['Chinese', '中文', '中国語', ],
  59. '英语': ['English', '英语', '英語', ],
  60. '日语': ['Japanese', '日语', '日本語', ],
  61. '韩语': ['Korean', '韩语', '韓国語', ],
  62. '法语': ['French', '法语', 'フランス語', ],
  63. '西班牙语': ['Spanish', '西班牙语', 'スペイン語', ],
  64. '意大利语': ['Italian', '意大利语', 'イタリア語', ],
  65. '德语': ['German', '德语', 'ドイツ語', ],
  66. '土耳其语': ['Turkish', '土耳其语', 'トルコ語', ],
  67. '俄语': ['Russian', '俄语', 'ロシア語', ],
  68. '葡萄牙语': ['Portuguese', '葡萄牙语', 'ポルトガル語', ],
  69. '越南语': ['Vietnamese', '越南语', 'ベトナム語', ],
  70. '印尼语': ['Indonesian', '印尼语', 'インドネシア語',],
  71. '泰语': ['Thai', '泰语', 'タイ語', ],
  72. '马来西亚语': ['Malaysian', '马来西亚语', 'マレーシア語', ],
  73. '阿拉伯语': ['Arabic', '阿拉伯语', 'アラビア語', ],
  74. '印地语': ['Hindi', '印地语', 'ヒンディー語', ],
  75. '翻译': ['Translate', '翻译', '翻訳', ],
  76. '人工翻译': ['by Human', '人工翻译', '翻訳家に依頼', ],
  77. };
  78. const CORRECTIONS = [
  79. (s) => s.replace(/h?tt?p(s?)[::]\/\/([^\s。]+)([。. ]*)/ig, 'http$1://$2'),/* for URL */
  80. (s) => s.replace(/([0-9]+):([0-9]+)/g, '$1:$2'),/* for 12:30 format */
  81. (s) => s.replace(/,([0-9]{3})/g, ',$1'),/* for 1,000,000 format */
  82. (s) => s.replace(/?/g, '?'),/* for URL */
  83. (s) => s.replace(/:/g, ':'),/* : */
  84. (s) => s.replace(/\.。/g, '。'),/* may be a bug */
  85. ];
  86. const SEPARATORS = ['\n:\n', '\n:\n', ':'];/*翻訳元, 翻訳先, 翻訳先span.textContent */
  87. const RETRY = 10;
  88. let site = {
  89. targets: {
  90. textpanelSource: () => $('.textpanel-source'),
  91. sourceTextarea: () => $('[node-type="source-textarea"]'),
  92. textpanelTargetTextblock: () => $('[node-type="textpanel-target-textblock"]'),
  93. sourceLanguageButton: () => $('[node-type="source_language_button"]'),
  94. exchangeLanguageButton: () => $('[node-type="exchange_language_button"]'),
  95. targetLanguageButton: () => $('[node-type="target_language_button"]'),
  96. sourceLanguageList: () => $('[node-type="source_language_list"]'),
  97. targetLanguageList: () => $('[node-type="target_language_list"]'),
  98. translateButton: () => $('[node-type="translate_button"]'),
  99. humanTranslation: () => $('[node-type="human-translation"]'),
  100. },
  101. get: {
  102. labels: () => {
  103. let index = LANGUAGES.findIndex(regexp => regexp.test(window.navigator.language)) || 0;
  104. let labels = LABELS;
  105. Object.keys(labels).forEach(key => labels[key] = labels[key][index]);
  106. return labels;
  107. },
  108. textSrcs: (textpanelTargetTextblock) => textpanelTargetTextblock.querySelectorAll('.text-src'),
  109. textDsts: (textpanelTargetTextblock) => textpanelTargetTextblock.querySelectorAll('.text-dst'),
  110. textMatrix: (textpanelTargetTextblock) => {
  111. return {
  112. srcs: Array.from(site.get.textSrcs(textpanelTargetTextblock)).map(e => e.textContent),
  113. dsts: Array.from(site.get.textDsts(textpanelTargetTextblock)).map(e => e.textContent),
  114. };
  115. },
  116. },
  117. set: {
  118. languageLabel: (node, labels) => {
  119. let span = node.querySelector('span'), label = span.textContent.replace(/\s/g, '');
  120. let replaced = createElement(core.html.languageLabel(labels[label] || span.textContent))
  121. span.parentNode.insertBefore(replaced, span);
  122. },
  123. languageButtonLabel: (button, labels) => {
  124. let label = button.textContent.replace(/\s/g, '');
  125. let buttonTextSpan = button.querySelector('.language-button-text');
  126. if(buttonTextSpan) buttonTextSpan.textContent = labels[label] || buttonTextSpan.textContent;
  127. else button.firstChild.data = labels[label] || button.firstChild.data;
  128. },
  129. translateButtonLabel: (button, labels) => {
  130. let label = button.textContent.replace(/\s/g, '');
  131. button.textContent = labels[label] || button.textContent;
  132. },
  133. humanTranslationLabel: (button, labels) => {
  134. let label = button.textContent.replace(/\s/g, '');
  135. button.lastChild.data = labels[label] || button.lastChild.data;
  136. },
  137. },
  138. };
  139. let html, elements = {}, timers = {}, sizes = {};
  140. let core = {
  141. initialize: function(){
  142. html = document.documentElement;
  143. html.classList.add(SCRIPTID);
  144. core.ready();
  145. core.addStyle();
  146. },
  147. ready: function(){
  148. core.getTargets(site.targets, RETRY).then(() => {
  149. log("I'm ready.");
  150. core.restoreMode();
  151. core.listenUserActions();
  152. core.replaceLabels();
  153. core.expandClickableArea();
  154. core.reloadOnWakeUp();
  155. });
  156. },
  157. restoreMode: function(){
  158. /* ページ読み込んだ時点で往復翻訳を有効に */
  159. let sourceTextarea = elements.sourceTextarea, translateButton = elements.translateButton;
  160. if(sourceTextarea.value.includes(SEPARATORS[0]) === true){
  161. translateButton.click();
  162. setTimeout(core.translateBackSwitch, 1000);
  163. }
  164. },
  165. listenUserActions: function(){
  166. window.addEventListener('keypress', function(e){
  167. switch(true){
  168. case(e.key === 'Enter' && e.shiftKey === true):
  169. core.translateSwitch();
  170. return e.preventDefault();
  171. case(e.key === 'Enter' && e.ctrlKey === true):
  172. core.translateBackSwitch();
  173. return e.preventDefault();
  174. }
  175. });
  176. window.addEventListener('focus', function(e){
  177. elements.sourceTextarea.focus();
  178. });
  179. },
  180. translateSwitch: function(){
  181. /* 翻訳言語の向きを入れ替える */
  182. let exchangeLanguageButton = elements.exchangeLanguageButton, sourceTextarea = elements.sourceTextarea;
  183. exchangeLanguageButton.click();
  184. sourceTextarea.focus();
  185. },
  186. translateBackSwitch: function(){
  187. /* 往復翻訳の有効無効を切り替える */
  188. let exchangeLanguageButton = elements.exchangeLanguageButton;
  189. if(exchangeLanguageButton.dataset.translateBack === 'true'){
  190. exchangeLanguageButton.dataset.translateBack = 'false';
  191. }else{
  192. exchangeLanguageButton.dataset.translateBack = 'true';
  193. core.translateBack();
  194. }
  195. },
  196. translateBack: function(){
  197. /* 往復翻訳する */
  198. let exchangeLanguageButton = elements.exchangeLanguageButton;
  199. let sourceTextarea = elements.sourceTextarea, textpanelTargetTextblock = elements.textpanelTargetTextblock;
  200. let sourceText = sourceTextarea.value, targetText = textpanelTargetTextblock.innerText, result = '';
  201. /* まだ往復翻訳してなければ */
  202. let selectionStart = sourceTextarea.selectionStart, selectionEnd = sourceTextarea.selectionEnd;/*カーソル位置を記憶*/
  203. if(sourceTextarea.value.includes(SEPARATORS[0]) === false){
  204. result = sourceText + SEPARATORS[0] + targetText;
  205. /* すでに往復翻訳済みなら */
  206. }else{
  207. sourceText = sourceText.slice(0, sourceText.indexOf(SEPARATORS[0]));
  208. targetText = targetText.slice(0, targetText.indexOf(SEPARATORS[1]));
  209. result = sourceText + SEPARATORS[0] + targetText;
  210. }
  211. /* 左辺の表示を完成させる */
  212. CORRECTIONS.forEach(c => result = c(result));
  213. sourceTextarea.value = result;
  214. sourceTextarea.dispatchEvent(new Event('input'));
  215. sourceTextarea.setSelectionRange(selectionStart, selectionEnd);
  216. /* 右辺の表示を追従させる */
  217. core.translateSwitch();
  218. if(textpanelTargetTextblock.dataset.status !== undefined) return;
  219. let compositing = false;
  220. let observer = observe(textpanelTargetTextblock, function(records){
  221. log(textpanelTargetTextblock.dataset.status, compositing, sourceTextarea.value.replace(/\n/g, ' '), textpanelTargetTextblock.innerText.replace(/\n/g, ' '));
  222. /* セパレータが消されたら往復翻訳モードを終了する */
  223. if(sourceTextarea.value.includes(SEPARATORS[0]) === false){
  224. exchangeLanguageButton.dataset.translateBack = 'false';
  225. delete(textpanelTargetTextblock.dataset.status);
  226. observer.disconnect();
  227. return;
  228. }
  229. switch(textpanelTargetTextblock.dataset.status){
  230. /* 往復を終えた最終翻訳が取得できたタイミング */
  231. case(undefined):
  232. case('back'):
  233. textpanelTargetTextblock.textMatrix = site.get.textMatrix(textpanelTargetTextblock);
  234. core.translateSwitch();
  235. textpanelTargetTextblock.dataset.status = 'go';
  236. break;
  237. /* 往路スタンバイに戻ったタイミング */
  238. case('go'):
  239. setTimeout(function(){
  240. let textDsts = site.get.textDsts(textpanelTargetTextblock);
  241. for(let i = Array.from(textDsts).findIndex(t => t.textContent === SEPARATORS[2]) + 1; textDsts[i]; i++){
  242. textDsts[i].textContent = textpanelTargetTextblock.textMatrix.dsts[i];
  243. }
  244. textpanelTargetTextblock.dataset.status = 'done';
  245. }, 1000);/*再度更新される場合があるので*/
  246. break;
  247. /* テキスト変更を検知して自動翻訳されたタイミング */
  248. case('done'):
  249. /* 原文も訳文も変化していなければ何も処理しない */
  250. if(sourceTextarea.value === sourceText && textpanelTargetTextblock.innerText === targetText) return;
  251. if(compositing === true) return;/*sourceTextとtargetTextは更新させない!*/
  252. sourceText = sourceTextarea.value, targetText = textpanelTargetTextblock.innerText;
  253. core.translateBack();
  254. textpanelTargetTextblock.dataset.status = 'back';
  255. break;
  256. }
  257. });
  258. sourceTextarea.addEventListener('compositionstart', function(e){
  259. compositing = true;
  260. });
  261. sourceTextarea.addEventListener('compositionend', function(e){
  262. compositing = false;
  263. });
  264. },
  265. replaceLabels: function(){
  266. let labels = site.get.labels();
  267. /* 翻訳言語リスト */
  268. let sourceLanguageList = elements.sourceLanguageList, targetLanguageList = elements.targetLanguageList;
  269. [sourceLanguageList, targetLanguageList].forEach(list => {
  270. Array.from(list.children).forEach(li => site.set.languageLabel(li, labels));
  271. });
  272. observe(targetLanguageList, function(records){
  273. Array.from(targetLanguageList.children).forEach(li => site.set.languageLabel(li, labels));
  274. });
  275. /* 翻訳言語 */
  276. let sourceLanguageButton = elements.sourceLanguageButton, targetLanguageButton = elements.targetLanguageButton;
  277. [sourceLanguageButton, targetLanguageButton].forEach(button => {
  278. site.set.languageButtonLabel(button, labels);
  279. observe(button, function(records){
  280. site.set.languageButtonLabel(button, labels);
  281. });
  282. });
  283. /* 翻訳ボタン */
  284. site.set.translateButtonLabel(elements.translateButton, labels);
  285. site.set.humanTranslationLabel(elements.humanTranslation, labels);
  286. },
  287. expandClickableArea: function(){
  288. let textpanelSource = elements.textpanelSource, sourceTextarea = elements.sourceTextarea;
  289. textpanelSource.addEventListener('click', function(e){
  290. sourceTextarea.focus();
  291. }, true);
  292. },
  293. reloadOnWakeUp: function(){
  294. let lastTime = Date.now();
  295. setInterval(function(){
  296. let now = Date.now();
  297. if(now - lastTime < 3*MINUTE) lastTime = now;
  298. else setTimeout(() => location.reload(), 1*MINUTE);/*ネットワークの復帰を待つ*/
  299. }, 1*MINUTE);
  300. },
  301. getTargets: function(targets, retry = 0){
  302. const get = function(resolve, reject, retry){
  303. for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
  304. let selected = targets[key]();
  305. if(selected){
  306. if(selected.length) selected.forEach((s) => s.dataset.selector = key);
  307. else selected.dataset.selector = key;
  308. elements[key] = selected;
  309. }else{
  310. if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
  311. log(`Not found: ${key}, retrying... (left ${retry})`);
  312. return setTimeout(get, 1000, resolve, reject, retry);
  313. }
  314. }
  315. resolve();
  316. };
  317. return new Promise(function(resolve, reject){
  318. get(resolve, reject, retry);
  319. });
  320. },
  321. addStyle: function(name = 'style'){
  322. if(core.html[name] === undefined) return;
  323. let style = createElement(core.html[name]());
  324. document.head.appendChild(style);
  325. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  326. elements[name] = style;
  327. },
  328. html: {
  329. languageLabel: (label) => `<span class="replaced">${label}</span>`,
  330. style: () => `
  331. <style type="text/css">
  332. /* 翻訳方向スイッチボタン */
  333. [data-selector="exchangeLanguageButton"]{
  334. border: 1px solid transparent;
  335. border-radius: 100%;
  336. width: 36px;
  337. height: 36px;
  338. }
  339. [data-selector="exchangeLanguageButton"][data-translate-back="true"]{
  340. border: 1px solid rgb(160, 76, 247);
  341. }
  342. /* クリッカブル領域を広げる */
  343. [data-selector="textpanelSource"]{
  344. cursor: text;
  345. }
  346. dummy/*core.expandClickableAreaでやる*/ [data-selector="sourceTextarea"]{
  347. height: 100% !important;
  348. }
  349. /* 往復翻訳処理中 */
  350. [data-selector="textpanelTargetTextblock"]{
  351. transition: opacity 125ms;
  352. }
  353. [data-selector="textpanelTargetTextblock"][data-status="back"],
  354. [data-selector="textpanelTargetTextblock"][data-status="go"]{
  355. animation: ${SCRIPTID}-blink 500ms ease infinite;
  356. }
  357. @keyframes ${SCRIPTID}-blink{
  358. 0%{opacity: .250}
  359. 100%{opacity: .125}
  360. }
  361. /* 翻訳言語リスト */
  362. [data-selector="sourceLanguageList"] > li > span.replaced,
  363. [data-selector="targetLanguageList"] > li > span.replaced{
  364. display: block;
  365. padding: 0 !important;
  366. margin: 0 1px !important;
  367. }
  368. [data-selector="sourceLanguageList"] > li > span.replaced + span,
  369. [data-selector="targetLanguageList"] > li > span.replaced + span{
  370. display: none;
  371. }
  372. /* 翻訳ボタン */
  373. [data-selector="humanTranslation"]{
  374. text-align: center;
  375. }
  376. [data-selector="humanTranslation"] .human-translation{
  377. display: inline-block;
  378. margin-left: auto;
  379. float: none;
  380. }
  381. </style>
  382. `,
  383. },
  384. };
  385. const setTimeout = window.setTimeout.bind(window), clearTimeout = window.clearTimeout.bind(window), setInterval = window.setInterval.bind(window), clearInterval = window.clearInterval.bind(window), requestAnimationFrame = window.requestAnimationFrame.bind(window);
  386. const alert = window.alert.bind(window), confirm = window.confirm.bind(window), prompt = window.prompt.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  387. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  388. class Storage{
  389. static key(key){
  390. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  391. }
  392. static save(key, value, expire = null){
  393. key = Storage.key(key);
  394. localStorage[key] = JSON.stringify({
  395. value: value,
  396. saved: Date.now(),
  397. expire: expire,
  398. });
  399. }
  400. static read(key){
  401. key = Storage.key(key);
  402. if(localStorage[key] === undefined) return undefined;
  403. let data = JSON.parse(localStorage[key]);
  404. if(data.value === undefined) return data;
  405. if(data.expire === undefined) return data;
  406. if(data.expire === null) return data.value;
  407. if(data.expire < Date.now()) return localStorage.removeItem(key);
  408. return data.value;
  409. }
  410. static delete(key){
  411. key = Storage.key(key);
  412. delete localStorage.removeItem(key);
  413. }
  414. static saved(key){
  415. key = Storage.key(key);
  416. if(localStorage[key] === undefined) return undefined;
  417. let data = JSON.parse(localStorage[key]);
  418. if(data.saved) return data.saved;
  419. else return undefined;
  420. }
  421. }
  422. const $ = function(s, f){
  423. let target = document.querySelector(s);
  424. if(target === null) return null;
  425. return f ? f(target) : target;
  426. };
  427. const $$ = function(s){return document.querySelectorAll(s)};
  428. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  429. const wait = function(ms){return new Promise((resolve) => setTimeout(resolve, ms))};
  430. const createElement = function(html = '<span></span>'){
  431. let outer = document.createElement('div');
  432. outer.innerHTML = html;
  433. return outer.firstElementChild;
  434. };
  435. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  436. let observer = new MutationObserver(callback.bind(element));
  437. observer.observe(element, options);
  438. return observer;
  439. };
  440. const normalize = function(string){
  441. return string.replace(/[!-~]/g, function(s){
  442. return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  443. }).replace(normalize.RE, function(s){
  444. return normalize.KANA[s];
  445. }).replace(/ /g, ' ').replace(/~/g, '〜');
  446. };
  447. normalize.KANA = {
  448. ガ:'ガ', ギ:'ギ', グ:'グ', ゲ:'ゲ', ゴ: 'ゴ',
  449. ザ:'ザ', ジ:'ジ', ズ:'ズ', ゼ:'ゼ', ゾ: 'ゾ',
  450. ダ:'ダ', ヂ:'ヂ', ヅ:'ヅ', デ:'デ', ド: 'ド',
  451. バ:'バ', ビ:'ビ', ブ:'ブ', ベ:'ベ', ボ: 'ボ',
  452. パ:'パ', ピ:'ピ', プ:'プ', ペ:'ペ', ポ: 'ポ',
  453. ヷ:'ヷ', ヺ:'ヺ', ヴ:'ヴ',
  454. ア:'ア', イ:'イ', ウ:'ウ', エ:'エ', オ:'オ',
  455. カ:'カ', キ:'キ', ク:'ク', ケ:'ケ', コ:'コ',
  456. サ:'サ', シ:'シ', ス:'ス', セ:'セ', ソ:'ソ',
  457. タ:'タ', チ:'チ', ツ:'ツ', テ:'テ', ト:'ト',
  458. ナ:'ナ', ニ:'ニ', ヌ:'ヌ', ネ:'ネ', ノ:'ノ',
  459. ハ:'ハ', ヒ:'ヒ', フ:'フ', ヘ:'ヘ', ホ:'ホ',
  460. マ:'マ', ミ:'ミ', ム:'ム', メ:'メ', モ:'モ',
  461. ヤ:'ヤ', ユ:'ユ', ヨ:'ヨ',
  462. ラ:'ラ', リ:'リ', ル:'ル', レ:'レ', ロ:'ロ',
  463. ワ:'ワ', ヲ:'ヲ', ン:'ン',
  464. ァ:'ァ', ィ:'ィ', ゥ:'ゥ', ェ:'ェ', ォ:'ォ',
  465. ッ:'ッ', ャ:'ャ', ュ:'ュ', ョ:'ョ',
  466. "。":'。', "、":'、', "ー":'ー', "「":'「', "」":'」', "・":'・',
  467. };
  468. normalize.RE = new RegExp('(' + Object.keys(normalize.KANA).join('|') + ')', 'g');
  469. const log = function(){
  470. if(!DEBUG) return;
  471. let l = log.last = log.now || new Date(), n = log.now = new Date();
  472. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  473. //console.log(error.stack);
  474. console.log(
  475. (SCRIPTID || '') + ':',
  476. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  477. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  478. /* :00 */ ':' + line,
  479. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  480. /* caller */ (callers[1] || '') + '()',
  481. ...arguments
  482. );
  483. };
  484. log.formats = [{
  485. name: 'Firefox Scratchpad',
  486. detector: /MARKER@Scratchpad/,
  487. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  488. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  489. }, {
  490. name: 'Firefox Console',
  491. detector: /MARKER@debugger/,
  492. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  493. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  494. }, {
  495. name: 'Firefox Greasemonkey 3',
  496. detector: /\/gm_scripts\//,
  497. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  498. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  499. }, {
  500. name: 'Firefox Greasemonkey 4+',
  501. detector: /MARKER@user-script:/,
  502. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  503. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  504. }, {
  505. name: 'Firefox Tampermonkey',
  506. detector: /MARKER@moz-extension:/,
  507. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  508. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  509. }, {
  510. name: 'Chrome Console',
  511. detector: /at MARKER \(<anonymous>/,
  512. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  513. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  514. }, {
  515. name: 'Chrome Tampermonkey',
  516. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  517. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
  518. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  519. }, {
  520. name: 'Chrome Extension',
  521. detector: /at MARKER \(chrome-extension:/,
  522. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  523. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  524. }, {
  525. name: 'Edge Console',
  526. detector: /at MARKER \(eval/,
  527. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  528. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  529. }, {
  530. name: 'Edge Tampermonkey',
  531. detector: /at MARKER \(Function/,
  532. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  533. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  534. }, {
  535. name: 'Safari',
  536. detector: /^MARKER$/m,
  537. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  538. getCallers: (e) => e.stack.split('\n'),
  539. }, {
  540. name: 'Default',
  541. detector: /./,
  542. getLine: (e) => 0,
  543. getCallers: (e) => [],
  544. }];
  545. log.format = log.formats.find(function MARKER(f){
  546. if(!f.detector.test(new Error().stack)) return false;
  547. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  548. return true;
  549. });
  550. const time = function(label){
  551. if(!DEBUG) return;
  552. const BAR = '|', TOTAL = 100;
  553. switch(true){
  554. case(label === undefined):/* time() to output total */
  555. let total = 0;
  556. Object.keys(time.records).forEach((label) => total += time.records[label].total);
  557. Object.keys(time.records).forEach((label) => {
  558. console.log(
  559. BAR.repeat((time.records[label].total / total) * TOTAL),
  560. label + ':',
  561. (time.records[label].total).toFixed(3) + 'ms',
  562. '(' + time.records[label].count + ')',
  563. );
  564. });
  565. time.records = {};
  566. break;
  567. case(!time.records[label]):/* time('label') to create and start the record */
  568. time.records[label] = {count: 0, from: performance.now(), total: 0};
  569. break;
  570. case(time.records[label].from === null):/* time('label') to re-start the lap */
  571. time.records[label].from = performance.now();
  572. break;
  573. case(0 < time.records[label].from):/* time('label') to add lap time to the record */
  574. time.records[label].total += performance.now() - time.records[label].from;
  575. time.records[label].from = null;
  576. time.records[label].count += 1;
  577. break;
  578. }
  579. };
  580. time.records = {};
  581. core.initialize();
  582. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  583. })();

QingJ © 2025

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