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