GreasyFork User Dashboard

It redesigns your own user page.

当前为 2019-01-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork User Dashboard
  3. // @name:ja GreasyFork User Dashboard
  4. // @namespace knoa.jp
  5. // @description It redesigns your own user page.
  6. // @description:ja 自分用の新しいユーザーページを提供します。
  7. // @include https://greasyfork.org/*/users/*
  8. // @version 1
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function(){
  13. const SCRIPTNAME = 'GreasyForkUserDashboard';
  14. const DEBUG = true;
  15. if(window === top && console.time) console.time(SCRIPTNAME);
  16. const INTERVAL = 1000;/* for fetch */
  17. const DEFAULTMAX = 10;/* for chart scale */
  18. const DAYS = 180;/* for chart length */
  19. const STATSUPDATE = 1000*60*60;/* stats update interval of greasyfork.org */
  20. const TRANSLATIONEXPIRE = 1000*60*60*24*30;/* cache time for translations */
  21. let site = {
  22. targets: {
  23. userSection: () => $('body > header + div > section:nth-of-type(1)'),
  24. controlPanel: () => $('#control-panel'),
  25. newScriptSetLink: () => $('a[href$="/sets/new"]'),
  26. scriptSets: () => $('body > header + div > section:nth-of-type(2)'),
  27. scripts: () => $('body > header + div > section:nth-of-type(2) + div'),
  28. userScriptSets: () => $('#user-script-sets'),
  29. userScriptList: () => $('#user-script-list'),
  30. },
  31. get: {
  32. language: (d) => d.documentElement.lang,
  33. firstScript: (list) => list.querySelector('li h2 > a'),
  34. translation: (d) => {return {
  35. info: d.querySelector('#script-links > li.current').textContent,
  36. code: d.querySelector('#script-links > li > a[href$="/code"]').textContent,
  37. history: d.querySelector('#script-links > li > a[href$="/versions"]').textContent,
  38. feedback: d.querySelector('#script-links > li > a[href$="/feedback"]').textContent.replace(/\s\(\d+\)/, ''),
  39. stats: d.querySelector('#script-links > li > a[href$="/stats"]').textContent,
  40. derivatives: d.querySelector('#script-links > li > a[href$="/derivatives"]').textContent,
  41. update: d.querySelector('#script-links > li > a[href$="/versions/new"]').textContent,
  42. delete: d.querySelector('#script-links > li > a[href$="/delete"]').textContent,
  43. admin: d.querySelector('#script-links > li > a[href$="/admin"]').textContent,
  44. version: d.querySelector('#script-stats > dt.script-show-version').textContent,
  45. }},
  46. props: (li) => {return {
  47. name: li.querySelector('h2 > a'),
  48. description: li.querySelector('.description'),
  49. stats: li.querySelector('dl.inline-script-stats'),
  50. dailyInstalls: li.querySelector('dd.script-list-daily-installs'),
  51. totalInstalls: li.querySelector('dd.script-list-total-installs'),
  52. ratings: li.querySelector('dd.script-list-ratings'),
  53. createdDate: li.querySelector('dd.script-list-created-date'),
  54. updatedDate: li.querySelector('dd.script-list-updated-date'),
  55. scriptVersion: li.dataset.scriptVersion,
  56. }},
  57. }
  58. };
  59. let translations = {
  60. 'en': {
  61. info: 'Info',
  62. code: 'Code',
  63. history: 'History',
  64. feedback: 'Feedback',
  65. stats: 'Stats',
  66. derivatives: 'Derivatives',
  67. update: 'Update',
  68. delete: 'Delete',
  69. admin: 'Admin',
  70. version: 'Version',
  71. }
  72. }, translation = translations['en'];
  73. let elements = {}, shown = {};
  74. let core = {
  75. initialize: function(){
  76. core.getElements();
  77. if(elements.length < site.targets.length) return log('Not user own page.');
  78. core.addStyle();
  79. core.getTranslations();
  80. core.hideUserSection();
  81. core.hideControlPanel();
  82. core.addTabNavigation();
  83. core.addNewScriptSetLink();
  84. core.rebuildScriptList();
  85. },
  86. getElements: function(){
  87. if(!site.targets.controlPanel()) return;/* not my own page */
  88. for(let i = 0, keys = Object.keys(site.targets); keys[i]; i++){
  89. let element = site.targets[keys[i]]();
  90. if(!element) return log(`Not found: ${keys[i]}`);
  91. element.dataset.selector = keys[i];
  92. elements[keys[i]] = element;
  93. }
  94. shown = Storage.read('shown') || shown;
  95. },
  96. getTranslations: function(){
  97. let language = site.get.language(document);
  98. translations = Storage.read('translations') || translations;
  99. translation = translations[language] || translation;
  100. if(site.get.language(document) === 'en' || Object.keys(translations).find((lang) => lang === language)) return;
  101. let firstScript = site.get.firstScript(elements.userScriptList);
  102. fetch(firstScript.href, {credentials: 'include'})
  103. .then(response => response.text())
  104. .then(text => new DOMParser().parseFromString(text, 'text/html'))
  105. .then(d => {
  106. translation = translations[site.get.language(d)] = site.get.translation(d);
  107. Storage.save('translations', translations, Date.now() + TRANSLATIONEXPIRE);
  108. });
  109. },
  110. hideUserSection: function(){
  111. let userSection = elements.userSection, more = createElement(core.html.more());
  112. if(!shown.userSection) userSection.classList.add('hidden');
  113. more.addEventListener('click', function(e){
  114. userSection.classList.toggle('hidden');
  115. shown.userSection = !userSection.classList.contains('hidden');
  116. Storage.save('shown', shown);
  117. });
  118. userSection.appendChild(more);
  119. },
  120. hideControlPanel: function(){
  121. let controlPanel = elements.controlPanel, header = controlPanel.firstElementChild;
  122. if(!shown.controlPanel) controlPanel.classList.add('hidden');
  123. elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
  124. header.addEventListener('click', function(e){
  125. controlPanel.classList.toggle('hidden');
  126. shown.controlPanel = !controlPanel.classList.contains('hidden');
  127. Storage.save('shown', shown);
  128. elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
  129. });
  130. },
  131. addTabNavigation: function(){
  132. const tabs = [
  133. {label: elements.scriptSets.querySelector('header').textContent, selector: 'scriptSets', list: elements.userScriptSets},
  134. {label: elements.scripts.querySelector('header').textContent, selector: 'scripts', list: elements.userScriptList, selected: true},
  135. ];
  136. let nav = createElement(core.html.tabNavigation()), scriptSets = elements.scriptSets;
  137. let template = nav.querySelector('li.template');
  138. scriptSets.parentNode.insertBefore(nav, scriptSets);
  139. for(let i = 0; tabs[i]; i++){
  140. let tab = template.cloneNode(true);
  141. tab.classList.remove('template');
  142. tab.textContent = tabs[i].label + ` (${tabs[i].list.children.length})`;
  143. tab.dataset.target = tabs[i].selector;
  144. tab.addEventListener('click', function(e){
  145. tab.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
  146. $('[data-tabified][data-selected="true"]').dataset.selected = 'false';
  147. tab.dataset.selected = 'true';
  148. $(`[data-selector="${tab.dataset.target}"]`).dataset.selected = 'true';
  149. });
  150. template.parentNode.insertBefore(tab, template);
  151. /**/
  152. let target = elements[tabs[i].selector];
  153. target.dataset.tabified = 'true';
  154. if(tabs[i].selected) tab.dataset.selected = target.dataset.selected = 'true';
  155. else tab.dataset.selected = target.dataset.selected = 'false';
  156. }
  157. },
  158. addNewScriptSetLink: function(){
  159. let link = elements.newScriptSetLink.cloneNode(true), list = elements.userScriptSets, li = document.createElement('li');
  160. li.appendChild(link);
  161. list.appendChild(li);
  162. },
  163. rebuildScriptList: function(){
  164. let stats = Storage.read('stats') || {}, promises = [];
  165. for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
  166. let more = createElement(core.html.more()), props = site.get.props(li);
  167. if(!shown[li.dataset.scriptName]) li.classList.add('hidden');
  168. more.addEventListener('click', function(e){
  169. li.classList.toggle('hidden');
  170. shown[li.dataset.scriptName] = !li.classList.contains('hidden');
  171. Storage.save('shown', shown);
  172. });
  173. li.appendChild(more);
  174. /* attatch titles */
  175. props.name.title = props.description.textContent.trim();
  176. props.dailyInstalls.previousElementSibling.title = props.dailyInstalls.previousElementSibling.textContent;
  177. props.totalInstalls.previousElementSibling.title = props.totalInstalls.previousElementSibling.textContent;
  178. props.ratings.previousElementSibling.title = props.ratings.previousElementSibling.textContent;
  179. props.createdDate.previousElementSibling.title = props.createdDate.previousElementSibling.textContent;
  180. props.updatedDate.previousElementSibling.title = props.updatedDate.previousElementSibling.textContent;
  181. /* wrap the description to make it an inline element */
  182. let span = document.createElement('span');
  183. span.textContent = props.name.title;
  184. props.description.replaceChild(span, props.description.firstChild);
  185. /* Link to Code from Version */
  186. let versionLabel = createElement(core.html.dt('script-list-version', translation.version));
  187. let versionLink = createElement(core.html.ddLink('script-list-version', props.scriptVersion, props.name.href + '/code', translation.code));
  188. versionLabel.title = versionLabel.textContent;
  189. props.stats.insertBefore(versionLabel, props.createdDate.previousElementSibling);
  190. props.stats.insertBefore(versionLink, props.createdDate.previousElementSibling);
  191. /* Link to Stats from Total installs */
  192. let statsLink = createElement(core.html.ddLink('script-list-total-installs', props.totalInstalls.textContent, props.name.href + '/stats', translation.stats));
  193. props.stats.replaceChild(statsLink, props.totalInstalls);
  194. /* Link to History from Updated date */
  195. let historyLink = createElement(core.html.ddLink('script-list-updated-date', props.updatedDate.textContent, props.name.href + '/versions', translation.history));
  196. props.stats.replaceChild(historyLink, props.updatedDate);
  197. /* Draw chart of daily update checks */
  198. let chart = createElement(core.html.chart());
  199. if(Object.keys(stats).length){
  200. core.buildChart(chart, stats[props.name.textContent].slice(-DAYS));
  201. li.appendChild(chart);
  202. continue;
  203. }else promises.push(new Promise(function(resolve, reject){
  204. setTimeout(function(){
  205. fetch(props.name.href + '/stats.csv')/* less file size than json */
  206. .then(response => response.text())
  207. .then(csv => {
  208. let lines = csv.split('\n');
  209. lines = lines.slice(1, -1);/* cut the labels + blank line */
  210. stats[props.name.textContent] = [];
  211. for(let i = 0; lines[i]; i++){
  212. let p = lines[i].split(',');
  213. stats[props.name.textContent][i] = {
  214. date: p[0],
  215. installs: parseInt(p[1]),
  216. updateChecks: parseInt(p[2]),
  217. };
  218. }
  219. core.buildChart(chart, stats[props.name.textContent].slice(-DAYS));
  220. li.appendChild(chart);
  221. resolve();
  222. });
  223. }, i * INTERVAL);/* server friendly */
  224. }));
  225. }
  226. Promise.all(promises)
  227. .then(() => {
  228. let now = Date.now(), past = now % STATSUPDATE, expire = now - past + STATSUPDATE;
  229. Storage.save('stats', stats, expire);
  230. });
  231. },
  232. buildChart: function(chart, stats){
  233. let max = DEFAULTMAX;
  234. for(let i = 0; stats[i]; i++){
  235. if(stats[i].updateChecks > max) max = stats[i].updateChecks;
  236. }
  237. let dl = chart.querySelector('dl'), dt = dl.querySelector('dt'), dd = dl.querySelector('dd');
  238. for(let i = 0, last = stats.length - 1; stats[i]; i++){
  239. let date = stats[i].date, installs = stats[i].installs, updateChecks = stats[i].updateChecks;
  240. let dateDt = dt.cloneNode(), countDd = dd.cloneNode();
  241. dateDt.classList.remove('template');
  242. countDd.classList.remove('template');
  243. dateDt.textContent = date;
  244. countDd.title = date + ': ' + updateChecks + (updateChecks === 1 ? ' check' : ' checks');
  245. countDd.style.height = ((updateChecks / max) * 100) + '%';
  246. if(i === last - 1){
  247. countDd.classList.add('last');
  248. let label = document.createElement('span');
  249. label.textContent = toMetric(updateChecks);
  250. countDd.appendChild(label);
  251. }
  252. dl.insertBefore(dateDt, dt);
  253. dl.insertBefore(countDd, dt);
  254. }
  255. },
  256. addStyle: function(name = 'style'){
  257. let style = createElement(core.html[name]());
  258. document.head.appendChild(style);
  259. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  260. elements[name] = style;
  261. },
  262. html: {
  263. more: () => `
  264. <button class="more"></button>
  265. `,
  266. tabNavigation: () => `
  267. <nav id="tabNavigation">
  268. <ul>
  269. <li class="template"></li>
  270. </ul>
  271. </nav>
  272. `,
  273. dt: (className, textContent) => `
  274. <dt class="${className}"><span>${textContent}</span></dt>
  275. `,
  276. ddLink: (className, textContent, href, title) => `
  277. <dd class="${className}"><a href="${href}" title="${title}">${textContent}</a></dd>
  278. `,
  279. chart: () => `
  280. <div class="chart">
  281. <dl>
  282. <dt class="template date"></dt>
  283. <dd class="template count"></dd>
  284. </dl>
  285. </div>
  286. `,
  287. style: () => `
  288. <style type="text/css">
  289. /* gray scale: 119-153-187-221 */
  290. /* coommon */
  291. h2, h3{
  292. margin: 0;
  293. }
  294. ul, ol{
  295. margin: 0;
  296. padding: 0 0 0 2em;
  297. }
  298. .template{
  299. display: none;
  300. }
  301. section.text-content{
  302. position: relative;
  303. padding: 0;
  304. }
  305. section.text-content > *{
  306. margin: 14px;
  307. }
  308. section.text-content h2{
  309. text-align: left !important;
  310. margin-bottom: 0;
  311. }
  312. section > header + *{
  313. margin: 0 0 14px !important;
  314. }
  315. button.more{
  316. color: rgb(153,153,153);
  317. background: white;
  318. padding: 0;
  319. cursor: pointer;
  320. }
  321. button.more::-moz-focus-inner{
  322. border: none;
  323. }
  324. button.more::after{
  325. font-size: medium;
  326. content: "▴";
  327. }
  328. .hidden > button.more{
  329. background: rgb(221, 221, 221);
  330. position: absolute;
  331. }
  332. .hidden > button.more::after{
  333. content: "▾";
  334. }
  335. /* User panel */
  336. section[data-selector="userSection"].hidden{
  337. max-height: 10em;
  338. overflow: hidden;
  339. }
  340. section[data-selector="userSection"] > button.more{
  341. position: relative;
  342. bottom: 0;
  343. width: 100%;
  344. margin: 0;
  345. border: none;
  346. border-top: 1px solid rgba(187, 187, 187);
  347. }
  348. section[data-selector="userSection"].hidden > button.more{
  349. position: absolute;
  350. }
  351. /* Control panel */
  352. section#control-panel{
  353. font-size: smaller;
  354. width: 200px;
  355. position: absolute;
  356. top: 0;
  357. right: 0;
  358. z-index: 1;
  359. }
  360. section#control-panel h3{
  361. font-size: 1em;
  362. padding: .25em 1em;
  363. border-radius: 5px 5px 0 0;
  364. background: rgb(103, 0, 0);
  365. color: white;
  366. cursor: pointer;
  367. }
  368. section#control-panel.hidden h3{
  369. border-radius: 5px 5px 5px 5px;
  370. }
  371. section#control-panel h3::after{
  372. content: " ▴";
  373. margin-left: .25em;
  374. }
  375. section#control-panel.hidden h3::after{
  376. content: " ▾";
  377. }
  378. ul#user-control-panel{
  379. list-style-type: square;
  380. color: rgb(187, 187, 187);
  381. width: 100%;
  382. margin: .5em 0;
  383. padding: .5em .5em .5em 1.5em;
  384. background: white;
  385. border-radius: 0 0 5px 5px;
  386. border: 1px solid rgb(187, 187, 187);
  387. border-top: none;
  388. box-sizing: border-box;
  389. }
  390. section#control-panel.hidden > ul#user-control-panel{
  391. display: none;
  392. }
  393. /* Discussions on your scripts */
  394. #user-discussions-on-scripts-written{
  395. margin-top: 0;
  396. }
  397. /* tabs */
  398. #tabNavigation > ul{
  399. list-style-type: none;
  400. padding: 0;
  401. display: flex;
  402. }
  403. #tabNavigation > ul > li{
  404. font-weight: bold;
  405. background: white;
  406. padding: .25em 1em;
  407. border: 1px solid rgb(187, 187, 187);
  408. border-bottom: none;
  409. border-radius: 5px 5px 0 0;
  410. box-shadow: 0 0 5px rgb(221, 221, 221);
  411. cursor: pointer;
  412. }
  413. #tabNavigation > ul > li:first-child{
  414. }
  415. #tabNavigation > ul > li[data-selected="false"]{
  416. color: rgb(153,153,153);
  417. background: rgb(221, 221, 221);
  418. }
  419. [data-selector="scriptSets"] > section,
  420. [data-tabified] #user-script-list{
  421. border-radius: 0 5px 5px 5px;
  422. }
  423. [data-tabified] header{
  424. display: none;
  425. }
  426. [data-tabified][data-selected="false"]{
  427. display: none;
  428. }
  429. /* Scripts */
  430. #user-script-list li{
  431. padding: .25em 1em;
  432. position: relative;
  433. }
  434. #user-script-list li:last-child{
  435. border-bottom: none;/* missing in greasyfork.org */
  436. }
  437. #user-script-list li article{
  438. position: relative;
  439. z-index: 1;/* over the .chart */
  440. pointer-events: none;
  441. }
  442. #user-script-list li article h2{
  443. margin-bottom: .25em;
  444. }
  445. #user-script-list li article h2 > a,
  446. #user-script-list li article h2 > .description/* it's block! */ > span,
  447. #user-script-list li article dl > dt > *,
  448. #user-script-list li article dl > dd > *{
  449. pointer-events: auto;/* apply on inline elements */
  450. }
  451. #user-script-list li button.more{
  452. border: 1px solid rgb(221,221,221);
  453. border-radius: 5px;
  454. position: absolute;
  455. top: 0;
  456. right: 0;
  457. margin: 5px;
  458. width: 2em;
  459. z-index: 1;/* over the .chart */
  460. }
  461. #user-script-list li .description{
  462. font-size: small;
  463. margin: 0 0 0 .1em;/* ajust first letter position */
  464. }
  465. #user-script-list li dl.inline-script-stats{
  466. column-count: 3;
  467. max-height: 3em;
  468. }
  469. #user-script-list li dl.inline-script-stats dt{
  470. overflow: hidden;
  471. white-space: nowrap;
  472. text-overflow: ellipsis;
  473. max-width: 200px;/* mysterious */
  474. }
  475. #user-script-list li dl.inline-script-stats .script-list-author{
  476. display: none;
  477. }
  478. #user-script-list li dl.inline-script-stats dt.script-list-daily-installs,
  479. #user-script-list li dl.inline-script-stats dt.script-list-total-installs{
  480. width: 65%;
  481. }
  482. #user-script-list li dl.inline-script-stats dd.script-list-daily-installs,
  483. #user-script-list li dl.inline-script-stats dd.script-list-total-installs{
  484. width: 35%;
  485. }
  486. #user-script-list li.hidden .description,
  487. #user-script-list li.hidden .inline-script-stats{
  488. display: none;
  489. }
  490. /* chart */
  491. .chart{
  492. position: absolute;
  493. top: 0;
  494. right: 0;
  495. width: 100%;
  496. height: 100%;
  497. overflow: hidden;
  498. mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
  499. -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
  500. }
  501. .chart > dl{
  502. position: absolute;
  503. bottom: 0;
  504. right: 2em;
  505. height: calc(100% - 5px);
  506. display: flex;
  507. align-items: flex-end;
  508. }
  509. .chart > dl > dt.date{
  510. display: none;
  511. }
  512. .chart > dl > dd.count{
  513. background: rgb(221,221,221);
  514. width: 3px;
  515. border-left: 1px solid white;
  516. margin: 0;
  517. }
  518. .chart > dl > dd.count.last,
  519. .chart > dl > dd.count:hover{
  520. background: rgb(187,187,187);
  521. }
  522. .chart > dl > dd.count.last:hover{
  523. background: rgb(153,153,153);
  524. }
  525. .chart > dl > dd.count.last > span{
  526. font-weight: bold;
  527. color: rgb(153,153,153);
  528. position: absolute;
  529. top: 5px;
  530. right: 10px;
  531. pointer-events: none;
  532. }
  533. .chart > dl > dd.count.last:hover > span{
  534. color: rgb(119,119,119);
  535. }
  536. /* sidebar */
  537. .sidebar{
  538. padding-top: 0;
  539. }
  540. .ad/* excuse me, it disappears only in my own user page :-) */,
  541. #script-list-filter{
  542. display: none !important;
  543. }
  544. </style>
  545. `,
  546. },
  547. };
  548. class Storage{
  549. static key(key){
  550. return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
  551. }
  552. static save(key, value, expire = null){
  553. key = Storage.key(key);
  554. localStorage[key] = JSON.stringify({
  555. value: value,
  556. saved: Date.now(),
  557. expire: expire,
  558. });
  559. }
  560. static read(key){
  561. key = Storage.key(key);
  562. if(localStorage[key] === undefined) return undefined;
  563. let data = JSON.parse(localStorage[key]);
  564. if(data.value === undefined) return data;
  565. if(data.expire === undefined) return data;
  566. if(data.expire === null) return data.value;
  567. if(data.expire < Date.now()) return localStorage.removeItem(key);
  568. return data.value;
  569. }
  570. static delete(key){
  571. key = Storage.key(key);
  572. delete localStorage.removeItem(key);
  573. }
  574. static saved(key){
  575. key = Storage.key(key);
  576. if(localStorage[key] === undefined) return undefined;
  577. let data = JSON.parse(localStorage[key]);
  578. if(data.saved) return data.saved;
  579. else return undefined;
  580. }
  581. }
  582. const $ = function(s){return document.querySelector(s)};
  583. const $$ = function(s){return document.querySelectorAll(s)};
  584. const createElement = function(html){
  585. let outer = document.createElement('div');
  586. outer.innerHTML = html;
  587. return outer.firstElementChild;
  588. };
  589. const toMetric = function(number, fixed = 1){
  590. switch(true){
  591. case(number < 1e3): return (number);
  592. case(number < 1e6): return (number/ 1e3).toFixed(fixed) + 'K';
  593. case(number < 1e9): return (number/ 1e6).toFixed(fixed) + 'M';
  594. case(number < 1e12): return (number/ 1e9).toFixed(fixed) + 'G';
  595. default: return (number/1e12).toFixed(fixed) + 'T';
  596. }
  597. };
  598. const log = function(){
  599. if(!DEBUG) return;
  600. let l = log.last = log.now || new Date(), n = log.now = new Date();
  601. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  602. //console.log(error.stack);
  603. console.log(
  604. SCRIPTNAME + ':',
  605. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  606. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  607. /* :00 */ ':' + line,
  608. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  609. /* caller */ (callers[1] || '') + '()',
  610. ...arguments
  611. );
  612. };
  613. log.formats = [{
  614. name: 'Firefox Scratchpad',
  615. detector: /MARKER@Scratchpad/,
  616. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  617. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  618. }, {
  619. name: 'Firefox Console',
  620. detector: /MARKER@debugger/,
  621. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  622. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  623. }, {
  624. name: 'Firefox Greasemonkey 3',
  625. detector: /\/gm_scripts\//,
  626. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  627. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  628. }, {
  629. name: 'Firefox Greasemonkey 4+',
  630. detector: /MARKER@user-script:/,
  631. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  632. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  633. }, {
  634. name: 'Firefox Tampermonkey',
  635. detector: /MARKER@moz-extension:/,
  636. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  637. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  638. }, {
  639. name: 'Chrome Console',
  640. detector: /at MARKER \(<anonymous>/,
  641. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  642. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  643. }, {
  644. name: 'Chrome Tampermonkey',
  645. detector: /at MARKER \((userscript\.html|chrome-extension:)/,
  646. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+)\)$/)[1] - 6,
  647. getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm),
  648. }, {
  649. name: 'Edge Console',
  650. detector: /at MARKER \(eval/,
  651. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  652. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  653. }, {
  654. name: 'Edge Tampermonkey',
  655. detector: /at MARKER \(Function/,
  656. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  657. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  658. }, {
  659. name: 'Safari',
  660. detector: /^MARKER$/m,
  661. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  662. getCallers: (e) => e.stack.split('\n'),
  663. }, {
  664. name: 'Default',
  665. detector: /./,
  666. getLine: (e) => 0,
  667. getCallers: (e) => [],
  668. }];
  669. log.format = log.formats.find(function MARKER(f){
  670. if(!f.detector.test(new Error().stack)) return false;
  671. //console.log('//// ' + f.name + '\n' + new Error().stack);
  672. return true;
  673. });
  674. core.initialize();
  675. if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
  676. })();