GreasyFork User Dashboard

It redesigns your own user page.

Від 26.01.2019. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        GreasyFork User Dashboard
// @name:ja     GreasyFork User Dashboard
// @namespace   knoa.jp
// @description It redesigns your own user page.
// @description:ja 自分用の新しいユーザーページを提供します。
// @include     https://greasyfork.org/*/users/*
// @version     1.0.2
// @grant       none
// ==/UserScript==

(function(){
  const SCRIPTNAME = 'GreasyForkUserDashboard';
  const DEBUG = false;/*
    1.0.2
    small fixes.
  */
  if(window === top && console.time) console.time(SCRIPTNAME);
  const INTERVAL = 1000;/* for fetch */
  const DEFAULTMAX = 10;/* for chart scale */
  const DAYS = 180;/* for chart length */
  const STATSUPDATE = 1000*60*60;/* stats update interval of greasyfork.org */
  const TRANSLATIONEXPIRE = 1000*60*60*24*30;/* cache time for translations */
  let site = {
    targets: {
      userSection: () => $('body > header + div > section:nth-of-type(1)'),
      controlPanel: () => $('#control-panel'),
      newScriptSetLink: () => $('a[href$="/sets/new"]'),
      scriptSets: () => $('body > header + div > section:nth-of-type(2)'),
      scripts: () => $('body > header + div > section:nth-of-type(2) + div'),
      userScriptSets: () => $('#user-script-sets'),
      userScriptList: () => $('#user-script-list'),
    },
    get: {
      language: (d) => d.documentElement.lang,
      firstScript: (list) => list.querySelector('li h2 > a'),
      translation: (d) => {return {
        info:        d.querySelector('#script-links > li.current').textContent,
        code:        d.querySelector('#script-links > li > a[href$="/code"]').textContent,
        history:     d.querySelector('#script-links > li > a[href$="/versions"]').textContent,
        feedback:    d.querySelector('#script-links > li > a[href$="/feedback"]').textContent.replace(/\s\(\d+\)/, ''),
        stats:       d.querySelector('#script-links > li > a[href$="/stats"]').textContent,
        derivatives: d.querySelector('#script-links > li > a[href$="/derivatives"]').textContent,
        update:      d.querySelector('#script-links > li > a[href$="/versions/new"]').textContent,
        delete:      d.querySelector('#script-links > li > a[href$="/delete"]').textContent,
        admin:       d.querySelector('#script-links > li > a[href$="/admin"]').textContent,
        version:     d.querySelector('#script-stats > dt.script-show-version').textContent,
      }},
      props: (li) => {return {
        name: li.querySelector('h2 > a'),
        description: li.querySelector('.description'),
        stats: li.querySelector('dl.inline-script-stats'),
        dailyInstalls: li.querySelector('dd.script-list-daily-installs'),
        totalInstalls: li.querySelector('dd.script-list-total-installs'),
        ratings: li.querySelector('dd.script-list-ratings'),
        createdDate: li.querySelector('dd.script-list-created-date'),
        updatedDate: li.querySelector('dd.script-list-updated-date'),
        scriptVersion: li.dataset.scriptVersion,
      }},
    }
  };
  let translations = {
    'en': {
      info:        'Info',
      code:        'Code',
      history:     'History',
      feedback:    'Feedback',
      stats:       'Stats',
      derivatives: 'Derivatives',
      update:      'Update',
      delete:      'Delete',
      admin:       'Admin',
      version:     'Version',
    }
  }, translation = translations['en'];
  let elements = {}, shown = {};
  let core = {
    initialize: function(){
      core.getElements();
      if(elements.length < site.targets.length) return log('Not user own page.');
      core.addStyle();
      core.getTranslations();
      core.hideUserSection();
      core.hideControlPanel();
      core.addTabNavigation();
      core.addNewScriptSetLink();
      core.rebuildScriptList();
    },
    getElements: function(){
      if(!site.targets.controlPanel()) return;/* not my own page */
      for(let i = 0, keys = Object.keys(site.targets); keys[i]; i++){
        let element = site.targets[keys[i]]();
        if(!element) return log(`Not found: ${keys[i]}`);
        element.dataset.selector = keys[i];
        elements[keys[i]] = element;
      }
      shown = Storage.read('shown') || shown;
    },
    getTranslations: function(){
      let language = site.get.language(document);
      translations = Storage.read('translations') || translations;
      translation = translations[language] || translation;
      if(site.get.language(document) === 'en' || Object.keys(translations).find((lang) => lang === language)) return;
      let firstScript = site.get.firstScript(elements.userScriptList);
      fetch(firstScript.href, {credentials: 'include'})
        .then(response => response.text())
        .then(text => new DOMParser().parseFromString(text, 'text/html'))
        .then(d => {
          translation = translations[site.get.language(d)] = site.get.translation(d);
          Storage.save('translations', translations, Date.now() + TRANSLATIONEXPIRE);
        });
    },
    hideUserSection: function(){
      let userSection = elements.userSection, more = createElement(core.html.more());
      if(!shown.userSection) userSection.classList.add('hidden');
      more.addEventListener('click', function(e){
        userSection.classList.toggle('hidden');
        shown.userSection = !userSection.classList.contains('hidden');
        Storage.save('shown', shown);
      });
      userSection.appendChild(more);
    },
    hideControlPanel: function(){
      let controlPanel = elements.controlPanel, header = controlPanel.firstElementChild;
      if(!shown.controlPanel) controlPanel.classList.add('hidden');
      elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
      header.addEventListener('click', function(e){
        controlPanel.classList.toggle('hidden');
        shown.controlPanel = !controlPanel.classList.contains('hidden');
        Storage.save('shown', shown);
        elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
      });
    },
    addTabNavigation: function(){
      const tabs = [
        {label: elements.scriptSets.querySelector('header').textContent, selector: 'scriptSets', list: elements.userScriptSets},
        {label: elements.scripts.querySelector('header').textContent, selector: 'scripts', list: elements.userScriptList, selected: true},
      ];
      let nav = createElement(core.html.tabNavigation()), scriptSets = elements.scriptSets;
      let template = nav.querySelector('li.template');
      scriptSets.parentNode.insertBefore(nav, scriptSets);
      for(let i = 0; tabs[i]; i++){
        let tab = template.cloneNode(true);
        tab.classList.remove('template');
        tab.textContent = tabs[i].label + ` (${tabs[i].list.children.length})`;
        tab.dataset.target = tabs[i].selector;
        tab.addEventListener('click', function(e){
          tab.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
          $('[data-tabified][data-selected="true"]').dataset.selected = 'false';
          tab.dataset.selected = 'true';
          $(`[data-selector="${tab.dataset.target}"]`).dataset.selected = 'true';
        });
        template.parentNode.insertBefore(tab, template);
        /**/
        let target = elements[tabs[i].selector];
        target.dataset.tabified = 'true';
        if(tabs[i].selected) tab.dataset.selected = target.dataset.selected = 'true';
        else tab.dataset.selected = target.dataset.selected = 'false';
      }
    },
    addNewScriptSetLink: function(){
      let link = elements.newScriptSetLink.cloneNode(true), list = elements.userScriptSets, li = document.createElement('li');
      li.appendChild(link);
      list.appendChild(li);
    },
    rebuildScriptList: function(){
      let stats = Storage.read('stats') || {}, promises = [];
      for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
        let more = createElement(core.html.more()), props = site.get.props(li);
        if(!shown[li.dataset.scriptName]) li.classList.add('hidden');
        more.addEventListener('click', function(e){
          li.classList.toggle('hidden');
          shown[li.dataset.scriptName] = !li.classList.contains('hidden');
          Storage.save('shown', shown);
        });
        li.appendChild(more);
        /* attatch titles */
        props.dailyInstalls.previousElementSibling.title = props.dailyInstalls.previousElementSibling.textContent;
        props.totalInstalls.previousElementSibling.title = props.totalInstalls.previousElementSibling.textContent;
        props.ratings.previousElementSibling.title       = props.ratings.previousElementSibling.textContent;
        props.createdDate.previousElementSibling.title   = props.createdDate.previousElementSibling.textContent;
        props.updatedDate.previousElementSibling.title   = props.updatedDate.previousElementSibling.textContent;
        /* wrap the description to make it an inline element */
        let span = document.createElement('span');
        span.textContent = props.description.textContent.trim();
        props.description.replaceChild(span, props.description.firstChild);
        /* Link to Code from Version */
        let versionLabel = createElement(core.html.dt('script-list-version', translation.version));
        let versionLink = createElement(core.html.ddLink('script-list-version', props.scriptVersion, props.name.href + '/code', translation.code));
        versionLabel.title = versionLabel.textContent;
        props.stats.insertBefore(versionLabel, props.createdDate.previousElementSibling);
        props.stats.insertBefore(versionLink, props.createdDate.previousElementSibling);
        /* Link to Stats from Total installs */
        let statsLink = createElement(core.html.ddLink('script-list-total-installs', props.totalInstalls.textContent, props.name.href + '/stats', translation.stats));
        props.stats.replaceChild(statsLink, props.totalInstalls);
        /* Link to History from Updated date */
        let historyLink = createElement(core.html.ddLink('script-list-updated-date', props.updatedDate.textContent, props.name.href + '/versions', translation.history));
        props.stats.replaceChild(historyLink, props.updatedDate);
        /* Draw chart of daily update checks */
        let chart = createElement(core.html.chart());
        if(stats[li.dataset.scriptName]){
          core.buildChart(chart, stats[li.dataset.scriptName].slice(-DAYS));
          li.appendChild(chart);
          continue;
        }else promises.push(new Promise(function(resolve, reject){
          setTimeout(function(){
            fetch(props.name.href + '/stats.csv')/* less file size than json */
              .then(response => response.text())
              .then(csv => {
                let lines = csv.split('\n');
                lines = lines.slice(1, -1);/* cut the labels + blank line */
                stats[props.name.textContent] = [];
                for(let i = 0; lines[i]; i++){
                  let p = lines[i].split(',');
                  stats[props.name.textContent][i] = {
                    date: p[0],
                    installs: parseInt(p[1]),
                    updateChecks: parseInt(p[2]),
                  };
                }
                core.buildChart(chart, stats[li.dataset.scriptName].slice(-DAYS));
                li.appendChild(chart);
                resolve();
              });
          }, i * INTERVAL);/* server friendly */
        }));
      }
      Promise.all(promises)
        .then(() => {
          let now = Date.now(), past = now % STATSUPDATE, expire = now - past + STATSUPDATE;
          Storage.save('stats', stats, expire);
        });
    },
    buildChart: function(chart, stats){
      let max = DEFAULTMAX;
      for(let i = 0; stats[i]; i++){
        if(stats[i].updateChecks > max) max = stats[i].updateChecks;
      }
      let dl = chart.querySelector('dl'), dt = dl.querySelector('dt'), dd = dl.querySelector('dd');
      for(let i = 0, last = stats.length - 1; stats[i]; i++){
        let date = stats[i].date, installs = stats[i].installs, updateChecks = stats[i].updateChecks;
        let dateDt = dt.cloneNode(), countDd = dd.cloneNode();
        dateDt.classList.remove('template');
        countDd.classList.remove('template');
        dateDt.textContent = date;
        countDd.title = date + ': ' + updateChecks + (updateChecks === 1 ? ' check' : ' checks');
        countDd.style.height = ((updateChecks / max) * 100) + '%';
        if(i === last - 1){
          countDd.classList.add('last');
          let label = document.createElement('span');
          label.textContent = toMetric(updateChecks);
          countDd.appendChild(label);
        }
        dl.insertBefore(dateDt, dt);
        dl.insertBefore(countDd, dt);
      }
    },
    addStyle: function(name = 'style'){
      let style = createElement(core.html[name]());
      document.head.appendChild(style);
      if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
      elements[name] = style;
    },
    html: {
      more: () => `
        <button class="more"></button>
      `,
      tabNavigation: () => `
        <nav id="tabNavigation">
          <ul>
            <li class="template"></li>
          </ul>
        </nav>
      `,
      dt: (className, textContent) => `
        <dt class="${className}"><span>${textContent}</span></dt>
      `,
      ddLink: (className, textContent, href, title) => `
        <dd class="${className}"><a href="${href}" title="${title}">${textContent}</a></dd>
      `,
      chart: () => `
        <div class="chart">
          <dl>
            <dt class="template date"></dt>
            <dd class="template count"></dd>
          </dl>
        </div>
      `,
      style: () => `
        <style type="text/css">
          /* gray scale: 119-153-187-221 */
          /* coommon */
          h2, h3{
            margin: 0;
          }
          ul, ol{
            margin: 0;
            padding: 0 0 0 2em;
          }
          .template{
            display: none;
          }
          section.text-content{
            position: relative;
            padding: 0;
          }
          section.text-content > *{
            margin: 14px;
          }
          section.text-content h2{
            text-align: left !important;
            margin-bottom: 0;
          }
          section > header + *{
            margin: 0 0 14px !important;
          }
          button.more{
            color: rgb(153,153,153);
            border: 1px solid rgb(187,187,187);
            background: white;
            padding: 0;
            cursor: pointer;
          }
          button.more::-moz-focus-inner{
            border: none;
          }
          button.more::after{
            font-size: medium;
            content: "▴";
          }
          .hidden > button.more{
            background: rgb(221, 221, 221);
            position: absolute;
          }
          .hidden > button.more::after{
            content: "▾";
          }
          /* User panel */
          section[data-selector="userSection"].hidden{
            max-height: 10em;
            overflow: hidden;
          }
          section[data-selector="userSection"] > button.more{
            position: relative;
            bottom: 0;
            width: 100%;
            margin: 0;
            border: none;
            border-top: 1px solid rgba(187, 187, 187);
            border-radius: 0 0 5px 5px;
          }
          section[data-selector="userSection"].hidden > button.more{
            position: absolute;
          }
          /* Control panel */
          section#control-panel{
            font-size: smaller;
            width: 200px;
            position: absolute;
            top: 0;
            right: 0;
            z-index: 1;
          }
          section#control-panel h3{
            font-size: 1em;
            padding: .25em 1em;
            border-radius: 5px 5px 0 0;
            background: rgb(103, 0, 0);
            color: white;
            cursor: pointer;
          }
          section#control-panel.hidden h3{
            border-radius: 5px 5px 5px 5px;
          }
          section#control-panel h3::after{
            content: " ▴";
            margin-left: .25em;
          }
          section#control-panel.hidden h3::after{
            content: " ▾";
          }
          ul#user-control-panel{
            list-style-type: square;
            color: rgb(187, 187, 187);
            width: 100%;
            margin: .5em 0;
            padding: .5em .5em .5em 1.5em;
            background: white;
            border-radius: 0 0 5px 5px;
            border: 1px solid rgb(187, 187, 187);
            border-top: none;
            box-sizing: border-box;
          }
          section#control-panel.hidden > ul#user-control-panel{
            display: none;
          }
          /* Discussions on your scripts */
          #user-discussions-on-scripts-written{
            margin-top: 0;
          }
          /* tabs */
          #tabNavigation > ul{
            list-style-type: none;
            padding: 0;
            display: flex;
          }
          #tabNavigation > ul > li{
            font-weight: bold;
            background: white;
            padding: .25em 1em;
            border: 1px solid rgb(187, 187, 187);
            border-bottom: none;
            border-radius: 5px 5px 0 0;
            box-shadow: 0 0 5px rgb(221, 221, 221);
            cursor: pointer;
          }
          #tabNavigation > ul > li:first-child{
          }
          #tabNavigation > ul > li[data-selected="false"]{
            color: rgb(153,153,153);
            background: rgb(221, 221, 221);
          }
          [data-selector="scriptSets"] > section,
          [data-tabified] #user-script-list{
            border-radius: 0 5px 5px 5px;
          }
          [data-tabified] header{
            display: none;
          }
          [data-tabified][data-selected="false"]{
            display: none;
          }
          /* Scripts */
          #user-script-list li{
            padding: .25em 1em;
            position: relative;
          }
          #user-script-list li:last-child{
            border-bottom: none;/* missing in greasyfork.org */
          }
          #user-script-list li article{
            position: relative;
            z-index: 1;/* over the .chart */
            pointer-events: none;
          }
          #user-script-list li article h2 > a,
          #user-script-list li article h2 > .description/* it's block! */ > span,
          #user-script-list li article dl > dt > *,
          #user-script-list li article dl > dd > *{
            pointer-events: auto;/* apply on inline elements */
          }
          #user-script-list li button.more{
            border-radius: 5px;
            position: absolute;
            top: 0;
            right: 0;
            margin: 5px;
            width: 2em;
            z-index: 1;/* over the .chart */
          }
          #user-script-list li .description{
            font-size: small;
            margin: 0 0 0 .1em;/* ajust first letter position */
          }
          #user-script-list li dl.inline-script-stats{
            margin-top: .25em;
            column-count: 3;
            max-height: 3em;
          }
          #user-script-list li dl.inline-script-stats dt{
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
            max-width: 200px;/* mysterious */
          }
          #user-script-list li dl.inline-script-stats .script-list-author{
            display: none;
          }
          #user-script-list li dl.inline-script-stats dt.script-list-daily-installs,
          #user-script-list li dl.inline-script-stats dt.script-list-total-installs{
            width: 65%;
          }
          #user-script-list li dl.inline-script-stats dd.script-list-daily-installs,
          #user-script-list li dl.inline-script-stats dd.script-list-total-installs{
            width: 35%;
          }
          #user-script-list li.hidden .description,
          #user-script-list li.hidden .inline-script-stats{
            display: none;
          }
          /* chart */
          .chart{
            position: absolute;
            top: 0;
            right: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
            -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
          }
          .chart > dl{
            position: absolute;
            bottom: 0;
            right: 2em;
            height: calc(100% - 5px);
            display: flex;
            align-items: flex-end;
          }
          .chart > dl > dt.date{
            display: none;
          }
          .chart > dl > dd.count{
            background: rgb(221,221,221);
            width: 3px;
            border-left: 1px solid white;
            margin: 0;
          }
          .chart > dl > dd.count.last,
          .chart > dl > dd.count:hover{
            background: rgb(187,187,187);
          }
          .chart > dl > dd.count.last:hover{
            background: rgb(153,153,153);
          }
          .chart > dl > dd.count.last > span{
            font-weight: bold;
            color: rgb(153,153,153);
            position: absolute;
            top: 5px;
            right: 10px;
            pointer-events: none;
          }
          .chart > dl > dd.count.last:hover > span{
            color: rgb(119,119,119);
          }
          /* sidebar */
          .sidebar{
            padding-top: 0;
          }
          .ad/* excuse me, it disappears only in my own user page :-) */,
          #script-list-filter{
            display: none !important;
          }
        </style>
      `,
    },
  };
  class Storage{
    static key(key){
      return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
    }
    static save(key, value, expire = null){
      key = Storage.key(key);
      localStorage[key] = JSON.stringify({
        value: value,
        saved: Date.now(),
        expire: expire,
      });
    }
    static read(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.value === undefined) return data;
      if(data.expire === undefined) return data;
      if(data.expire === null) return data.value;
      if(data.expire < Date.now()) return localStorage.removeItem(key);
      return data.value;
    }
    static delete(key){
      key = Storage.key(key);
      delete localStorage.removeItem(key);
    }
    static saved(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.saved) return data.saved;
      else return undefined;
    }
  }
  const $ = function(s){return document.querySelector(s)};
  const $$ = function(s){return document.querySelectorAll(s)};
  const createElement = function(html){
    let outer = document.createElement('div');
    outer.innerHTML = html;
    return outer.firstElementChild;
  };
  const toMetric = function(number, fixed = 1){
    switch(true){
      case(number <  1e3): return (number);
      case(number <  1e6): return (number/ 1e3).toFixed(fixed) + 'K';
      case(number <  1e9): return (number/ 1e6).toFixed(fixed) + 'M';
      case(number < 1e12): return (number/ 1e9).toFixed(fixed) + 'G';
      default:             return (number/1e12).toFixed(fixed) + 'T';
    }
  };
  const log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
    //console.log(error.stack);
    console.log(
      SCRIPTNAME + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + line,
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '') + '()',
      ...arguments
    );
  };
  log.formats = [{
      name: 'Firefox Scratchpad',
      detector: /MARKER@Scratchpad/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Console',
      detector: /MARKER@debugger/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 3',
      detector: /\/gm_scripts\//,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 4+',
      detector: /MARKER@user-script:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Tampermonkey',
      detector: /MARKER@moz-extension:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Chrome Console',
      detector: /at MARKER \(<anonymous>/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
    }, {
      name: 'Chrome Tampermonkey',
      detector: /at MARKER \((userscript\.html|chrome-extension:)/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+)\)$/)[1] - 6,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm),
    }, {
      name: 'Edge Console',
      detector: /at MARKER \(eval/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
    }, {
      name: 'Edge Tampermonkey',
      detector: /at MARKER \(Function/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
    }, {
      name: 'Safari',
      detector: /^MARKER$/m,
      getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
      getCallers: (e) => e.stack.split('\n'),
    }, {
      name: 'Default',
      detector: /./,
      getLine: (e) => 0,
      getCallers: (e) => [],
    }];
  log.format = log.formats.find(function MARKER(f){
    if(!f.detector.test(new Error().stack)) return false;
    //console.log('//// ' + f.name + '\n' + new Error().stack);
    return true;
  });
  core.initialize();
  if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
})();