GreasyFork User Dashboard

It redesigns your own user page.

Versión del día 26/01/2019. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
})();