GreasyFork User Dashboard

It redesigns your own user page.

Per 26-01-2019. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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