GreasyFork User Dashboard

It redesigns your own user page.

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.1
// @grant       none
// ==/UserScript==

(function(){
  const SCRIPTNAME = 'GreasyForkUserDashboard';
  const DEBUG = false;/*
    1.0.1
    bug fix.
  */
  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.name.title = props.description.textContent.trim();
        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.name.title;
        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);
            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);
          }
          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{
            margin-bottom: .25em;
          }
          #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: 1px solid rgb(221,221,221);
            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{
            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);
})();