<commit_before>/* globals marked, unfetch, ES6Promise */

if (!window.Promise) {
  window.Promise = ES6Promise;
}
if (!window.fetch) {
  window.fetch = unfetch;
}

onunhandledrejection = function (e) {
  throw e.reason;
};

var $loadingElem = document.querySelector('#loading');
var $mainElem = document.querySelector('#main');
var $markdownElem = document.querySelector('#markdown');
var $markedVerElem = document.querySelector('#markedVersion');
var $commitVerElem = document.querySelector('#commitVersion');
var $markedVer = document.querySelector('#markedCdn');
var $optionsElem = document.querySelector('#options');
var $outputTypeElem = document.querySelector('#outputType');
var $inputTypeElem = document.querySelector('#inputType');
var $responseTimeElem = document.querySelector('#responseTime');
var $previewElem = document.querySelector('#preview');
var $previewIframe = document.querySelector('#preview iframe');
var $permalinkElem = document.querySelector('#permalink');
var $clearElem = document.querySelector('#clear');
var $htmlElem = document.querySelector('#html');
var $lexerElem = document.querySelector('#lexer');
var $panes = document.querySelectorAll('.pane');
var $inputPanes = document.querySelectorAll('.inputPane');
var lastInput = '';
var inputDirty = true;
var $activeOutputElem = null;
var search = searchToObject();
var markedVersions = {
  master: 'https://cdn.jsdelivr.net/gh/markedjs/marked/lib/marked.js'
};
var markedVersionCache = {};
var delayTime = 1;
var checkChangeTimeout = null;
var markedWorker;

$previewIframe.addEventListener('load', handleIframeLoad);

$outputTypeElem.addEventListener('change', handleOutputChange, false);

$inputTypeElem.addEventListener('change', handleInputChange, false);

$markedVerElem.addEventListener('change', handleVersionChange, false);

$markdownElem.addEventListener('change', handleInput, false);
$markdownElem.addEventListener('keyup', handleInput, false);
$markdownElem.addEventListener('keypress', handleInput, false);
$markdownElem.addEventListener('keydown', handleInput, false);

$optionsElem.addEventListener('change', handleInput, false);
$optionsElem.addEventListener('keyup', handleInput, false);
$optionsElem.addEventListener('keypress', handleInput, false);
$optionsElem.addEventListener('keydown', handleInput, false);

$commitVerElem.style.display = 'none';
$commitVerElem.addEventListener('keypress', handleAddVersion, false);

$clearElem.addEventListener('click', handleClearClick, false);

Promise.all([
  setInitialQuickref(),
  setInitialOutputType(),
  setInitialText(),
  setInitialVersion()
    .then(setInitialOptions)
]).then(function () {
  handleInputChange();
  handleOutputChange();
  checkForChanges();
  setScrollPercent(0);
  $loadingElem.style.display = 'none';
  $mainElem.style.display = 'block';
});

function setInitialText() {
  if ('text' in search) {
    $markdownElem.value = search.text;
  } else {
    return fetch('./initial.md')
      .then(function (res) { return res.text(); })
      .then(function (text) {
        if ($markdownElem.value === '') {
          $markdownElem.value = text;
        }
      });
  }
}

function setInitialQuickref() {
  return fetch('./quickref.md')
    .then(function (res) { return res.text(); })
    .then(function (text) {
      document.querySelector('#quickref').value = text;
    });
}

function setInitialVersion() {
  return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
    .then(function (res) {
      return res.json();
    })
    .then(function (json) {
      for (var i = 0; i < json.versions.length; i++) {
        var ver = json.versions[i];
        markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/lib/marked.js';
        var opt = document.createElement('option');
        opt.textContent = ver;
        opt.value = ver;
        $markedVerElem.appendChild(opt);
      }
    })
    .then(function () {
      if (search.version) {
        if (!markedVersions[search.version]) {
          var match = search.version.match(/^(\w+):(.+)$/);
          if (match) {
            switch (match[1]) {
              case 'commit':
                addCommitVersion(search.version, match[2].substring(0, 7), match[2]);
                return search.version;
              case 'pr':
                return getPrCommit(match[2])
                  .then(function (commit) {
                    if (!commit) {
                      return 'master';
                    }
                    addCommitVersion(search.version, 'PR #' + match[2], commit);
                    return search.version;
                  });
            }
          }
        }
      }

      return 'master';
    })
    .then(function (version) {
      $markedVerElem.value = version;
    })
    .then(updateVersion);
}

function setInitialOptions() {
  if ('options' in search) {
    $optionsElem.value = search.options;
  } else {
    setDefaultOptions();
  }
}

function setInitialOutputType() {
  if (search.outputType) {
    $outputTypeElem.value = search.outputType;
  }
}

function handleIframeLoad() {
  lastInput = '';
  inputDirty = true;
}

function handleInput() {
  inputDirty = true;
};

function handleVersionChange() {
  if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') {
    $commitVerElem.style.display = '';
  } else {
    $commitVerElem.style.display = 'none';
    updateVersion();
  }
}

function handleClearClick() {
  $markdownElem.value = '';
  $markedVerElem.value = 'master';
  $commitVerElem.style.display = 'none';
  updateVersion().then(setDefaultOptions);
}

function handleAddVersion(e) {
  if (e.which === 13) {
    switch ($markedVerElem.value) {
      case 'commit':
        var commit = $commitVerElem.value.toLowerCase();
        if (!commit.match(/^[0-9a-f]{40}$/)) {
          alert('That is not a valid commit');
          return;
        }
        addCommitVersion('commit:' + commit, commit.substring(0, 7), commit);
        $markedVerElem.value = 'commit:' + commit;
        $commitVerElem.style.display = 'none';
        $commitVerElem.value = '';
        updateVersion();
        break;
      case 'pr':
        $commitVerElem.disabled = true;
        var pr = $commitVerElem.value.replace(/\D/g, '');
        getPrCommit(pr)
          .then(function (commit) {
            $commitVerElem.disabled = false;
            if (!commit) {
              alert('That is not a valid PR');
              return;
            }
            addCommitVersion('pr:' + pr, 'PR #' + pr, commit);
            $markedVerElem.value = 'pr:' + pr;
            $commitVerElem.style.display = 'none';
            $commitVerElem.value = '';
            updateVersion();
          });
    }
  }
}

function handleInputChange() {
  handleChange($inputPanes, $inputTypeElem.value);
}

function handleOutputChange() {
  $activeOutputElem = handleChange($panes, $outputTypeElem.value);
  updateLink();
}

function handleChange(panes, visiblePane) {
  var active = null;
  for (var i = 0; i < panes.length; i++) {
    if (panes[i].id === visiblePane) {
      panes[i].style.display = '';
      active = panes[i];
    } else {
      panes[i].style.display = 'none';
    }
  }
  return active;
};

function addCommitVersion(value, text, commit) {
  if (markedVersions[value]) {
    return;
  }
  markedVersions[value] = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + commit + '/lib/marked.js';
  var opt = document.createElement('option');
  opt.textContent = text;
  opt.value = value;
  $markedVerElem.insertBefore(opt, $markedVerElem.firstChild);
}

function getPrCommit(pr) {
  return fetch('https://api.github.com/repos/markedjs/marked/pulls/' + pr + '/commits')
    .then(function (res) {
      return res.json();
    })
    .then(function (json) {
      return json[json.length - 1].sha;
    }).catch(function () {
      // return undefined
    });
}

function setDefaultOptions() {
  if (window.Worker) {
    messageWorker({
      task: 'defaults',
      version: markedVersions[$markedVerElem.value]
    });
  } else {
    var defaults = marked.getDefaults();
    setOptions(defaults);
  }
}

function setOptions(opts) {
  $optionsElem.value = JSON.stringify(
    opts,
    function (key, value) {
      if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
        return undefined;
      }
      return value;
    }, ' ');
}

function searchToObject() {
  // modified from https://stackoverflow.com/a/7090123/806777
  var pairs = location.search.slice(1).split('&');
  var obj = {};

  for (var i = 0; i < pairs.length; i++) {
    if (pairs[i] === '') {
      continue;
    }

    var pair = pairs[i].split('=');

    obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
  }

  return obj;
}

function jsonString(input) {
  var output = (input + '')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r')
    .replace(/\t/g, '\\t')
    .replace(/\f/g, '\\f')
    .replace(/[\\"']/g, '\\$&')
    .replace(/\u0000/g, '\\0');
  return '"' + output + '"';
};

function getScrollSize() {
  var e = $activeOutputElem;

  return e.scrollHeight - e.clientHeight;
};

function getScrollPercent() {
  var size = getScrollSize();

  if (size <= 0) {
    return 1;
  }

  return $activeOutputElem.scrollTop / size;
};

function setScrollPercent(percent) {
  $activeOutputElem.scrollTop = percent * getScrollSize();
};

function updateLink() {
  var outputType = '';
  if ($outputTypeElem.value !== 'preview') {
    outputType = 'outputType=' + $outputTypeElem.value + '&';
  }

  $permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
      + '&options=' + encodeURIComponent($optionsElem.value)
      + '&version=' + encodeURIComponent($markedVerElem.value);
  history.replaceState('', document.title, $permalinkElem.href);
}

function updateVersion() {
  if (window.Worker) {
    handleInput();
    return Promise.resolve();
  }
  var promise;
  if (markedVersionCache[$markedVerElem.value]) {
    promise = Promise.resolve(markedVersionCache[$markedVerElem.value]);
  } else {
    promise = fetch(markedVersions[$markedVerElem.value])
      .then(function (res) { return res.text(); })
      .then(function (text) {
        markedVersionCache[$markedVerElem.value] = text;
        return text;
      });
  }
  return promise.then(function (text) {
    var script = document.createElement('script');
    script.textContent = text;

    $markedVer.parentNode.replaceChild(script, $markedVer);
    $markedVer = script;
  }).then(handleInput);
}

function checkForChanges() {
  if (inputDirty && $markedVerElem.value !== 'commit' && $markedVerElem.value !== 'pr' && (typeof marked !== 'undefined' || window.Worker)) {
    inputDirty = false;

    updateLink();

    var options = {};
    var optionsString = $optionsElem.value || '{}';
    try {
      var newOptions = JSON.parse(optionsString);
      options = newOptions;
      $optionsElem.classList.remove('error');
    } catch (err) {
      $optionsElem.classList.add('error');
    }

    var version = markedVersions[$markedVerElem.value];
    var markdown = $markdownElem.value;
    var hash = version + markdown + optionsString;
    if (lastInput !== hash) {
      lastInput = hash;
      if (window.Worker) {
        delayTime = 100;
        messageWorker({
          task: 'parse',
          version: version,
          markdown: markdown,
          options: options
        });
      } else {
        var startTime = new Date();
        var lexed = marked.lexer(markdown, options);
        var lexedList = [];
        for (var i = 0; i < lexed.length; i++) {
          var lexedLine = [];
          for (var j in lexed[i]) {
            lexedLine.push(j + ':' + jsonString(lexed[i][j]));
          }
          lexedList.push('{' + lexedLine.join(', ') + '}');
        }
        var parsed = marked.parser(lexed, options);
        var scrollPercent = getScrollPercent();
        setParsed(parsed, lexedList.join('\n'));
        setScrollPercent(scrollPercent);
        var endTime = new Date();
        delayTime = endTime - startTime;
        setResponseTime(delayTime);
        if (delayTime < 50) {
          delayTime = 50;
        } else if (delayTime > 500) {
          delayTime = 1000;
        }
      }
    }
  }
  checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
};

function setResponseTime(ms) {
  var amount = ms;
  var suffix = 'ms';
  if (ms > 1000 * 60 * 60) {
    amount = 'Too Long';
    suffix = '';
  } else if (ms > 1000 * 60) {
    amount = '>' + Math.floor(ms / (1000 * 60));
    suffix = 'm';
  } else if (ms > 1000) {
    amount = '>' + Math.floor(ms / 1000);
    suffix = 's';
  }
  $responseTimeElem.textContent = amount + suffix;
}

function setParsed(parsed, lexed) {
  try {
    $previewIframe.contentDocument.body.innerHTML = parsed;
  } catch (ex) {}
  $htmlElem.value = parsed;
  $lexerElem.value = lexed;
}

function messageWorker(message) {
  if (!markedWorker || markedWorker.working) {
    if (markedWorker) {
      clearTimeout(markedWorker.timeout);
      markedWorker.terminate();
    }
    markedWorker = new Worker('worker.js');
    markedWorker.onmessage = function (e) {
      clearTimeout(markedWorker.timeout);
      markedWorker.working = false;
      switch (e.data.task) {
        case 'defaults':
          setOptions(e.data.defaults);
          break;
        case 'parse':
          $previewElem.classList.remove('error');
          $htmlElem.classList.remove('error');
          $lexerElem.classList.remove('error');
          var scrollPercent = getScrollPercent();
          setParsed(e.data.parsed, e.data.lexed);
          setScrollPercent(scrollPercent);
          setResponseTime(e.data.time);
          break;
      }
      clearTimeout(checkChangeTimeout);
      delayTime = 10;
      checkForChanges();
    };
    markedWorker.onerror = markedWorker.onmessageerror = function (err) {
      clearTimeout(markedWorker.timeout);
      var error = 'There was an error in the Worker';
      if (err) {
        if (err.message) {
          error = err.message;
        } else {
          error = err;
        }
      }
      error = error.replace(/^Uncaught Error: /, '');
      $previewElem.classList.add('error');
      $htmlElem.classList.add('error');
      $lexerElem.classList.add('error');
      setParsed(error, error);
      setScrollPercent(0);
    };
  }
  if (message.task !== 'defaults') {
    markedWorker.working = true;
    workerTimeout(0);
  }
  markedWorker.postMessage(message);
}

function workerTimeout(seconds) {
  markedWorker.timeout = setTimeout(function () {
    seconds++;
    markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
    workerTimeout(seconds);
  }, 1000);
}
<commit_msg>use @master<commit_after>/* globals marked, unfetch, ES6Promise */

if (!window.Promise) {
  window.Promise = ES6Promise;
}
if (!window.fetch) {
  window.fetch = unfetch;
}

onunhandledrejection = function (e) {
  throw e.reason;
};

var $loadingElem = document.querySelector('#loading');
var $mainElem = document.querySelector('#main');
var $markdownElem = document.querySelector('#markdown');
var $markedVerElem = document.querySelector('#markedVersion');
var $commitVerElem = document.querySelector('#commitVersion');
var $markedVer = document.querySelector('#markedCdn');
var $optionsElem = document.querySelector('#options');
var $outputTypeElem = document.querySelector('#outputType');
var $inputTypeElem = document.querySelector('#inputType');
var $responseTimeElem = document.querySelector('#responseTime');
var $previewElem = document.querySelector('#preview');
var $previewIframe = document.querySelector('#preview iframe');
var $permalinkElem = document.querySelector('#permalink');
var $clearElem = document.querySelector('#clear');
var $htmlElem = document.querySelector('#html');
var $lexerElem = document.querySelector('#lexer');
var $panes = document.querySelectorAll('.pane');
var $inputPanes = document.querySelectorAll('.inputPane');
var lastInput = '';
var inputDirty = true;
var $activeOutputElem = null;
var search = searchToObject();
var markedVersions = {
  master: 'https://cdn.jsdelivr.net/gh/markedjs/marked@master/lib/marked.js'
};
var markedVersionCache = {};
var delayTime = 1;
var checkChangeTimeout = null;
var markedWorker;

$previewIframe.addEventListener('load', handleIframeLoad);

$outputTypeElem.addEventListener('change', handleOutputChange, false);

$inputTypeElem.addEventListener('change', handleInputChange, false);

$markedVerElem.addEventListener('change', handleVersionChange, false);

$markdownElem.addEventListener('change', handleInput, false);
$markdownElem.addEventListener('keyup', handleInput, false);
$markdownElem.addEventListener('keypress', handleInput, false);
$markdownElem.addEventListener('keydown', handleInput, false);

$optionsElem.addEventListener('change', handleInput, false);
$optionsElem.addEventListener('keyup', handleInput, false);
$optionsElem.addEventListener('keypress', handleInput, false);
$optionsElem.addEventListener('keydown', handleInput, false);

$commitVerElem.style.display = 'none';
$commitVerElem.addEventListener('keypress', handleAddVersion, false);

$clearElem.addEventListener('click', handleClearClick, false);

Promise.all([
  setInitialQuickref(),
  setInitialOutputType(),
  setInitialText(),
  setInitialVersion()
    .then(setInitialOptions)
]).then(function () {
  handleInputChange();
  handleOutputChange();
  checkForChanges();
  setScrollPercent(0);
  $loadingElem.style.display = 'none';
  $mainElem.style.display = 'block';
});

function setInitialText() {
  if ('text' in search) {
    $markdownElem.value = search.text;
  } else {
    return fetch('./initial.md')
      .then(function (res) { return res.text(); })
      .then(function (text) {
        if ($markdownElem.value === '') {
          $markdownElem.value = text;
        }
      });
  }
}

function setInitialQuickref() {
  return fetch('./quickref.md')
    .then(function (res) { return res.text(); })
    .then(function (text) {
      document.querySelector('#quickref').value = text;
    });
}

function setInitialVersion() {
  return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
    .then(function (res) {
      return res.json();
    })
    .then(function (json) {
      for (var i = 0; i < json.versions.length; i++) {
        var ver = json.versions[i];
        markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/lib/marked.js';
        var opt = document.createElement('option');
        opt.textContent = ver;
        opt.value = ver;
        $markedVerElem.appendChild(opt);
      }
    })
    .then(function () {
      if (search.version) {
        if (!markedVersions[search.version]) {
          var match = search.version.match(/^(\w+):(.+)$/);
          if (match) {
            switch (match[1]) {
              case 'commit':
                addCommitVersion(search.version, match[2].substring(0, 7), match[2]);
                return search.version;
              case 'pr':
                return getPrCommit(match[2])
                  .then(function (commit) {
                    if (!commit) {
                      return 'master';
                    }
                    addCommitVersion(search.version, 'PR #' + match[2], commit);
                    return search.version;
                  });
            }
          }
        }
      }

      return 'master';
    })
    .then(function (version) {
      $markedVerElem.value = version;
    })
    .then(updateVersion);
}

function setInitialOptions() {
  if ('options' in search) {
    $optionsElem.value = search.options;
  } else {
    setDefaultOptions();
  }
}

function setInitialOutputType() {
  if (search.outputType) {
    $outputTypeElem.value = search.outputType;
  }
}

function handleIframeLoad() {
  lastInput = '';
  inputDirty = true;
}

function handleInput() {
  inputDirty = true;
};

function handleVersionChange() {
  if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') {
    $commitVerElem.style.display = '';
  } else {
    $commitVerElem.style.display = 'none';
    updateVersion();
  }
}

function handleClearClick() {
  $markdownElem.value = '';
  $markedVerElem.value = 'master';
  $commitVerElem.style.display = 'none';
  updateVersion().then(setDefaultOptions);
}

function handleAddVersion(e) {
  if (e.which === 13) {
    switch ($markedVerElem.value) {
      case 'commit':
        var commit = $commitVerElem.value.toLowerCase();
        if (!commit.match(/^[0-9a-f]{40}$/)) {
          alert('That is not a valid commit');
          return;
        }
        addCommitVersion('commit:' + commit, commit.substring(0, 7), commit);
        $markedVerElem.value = 'commit:' + commit;
        $commitVerElem.style.display = 'none';
        $commitVerElem.value = '';
        updateVersion();
        break;
      case 'pr':
        $commitVerElem.disabled = true;
        var pr = $commitVerElem.value.replace(/\D/g, '');
        getPrCommit(pr)
          .then(function (commit) {
            $commitVerElem.disabled = false;
            if (!commit) {
              alert('That is not a valid PR');
              return;
            }
            addCommitVersion('pr:' + pr, 'PR #' + pr, commit);
            $markedVerElem.value = 'pr:' + pr;
            $commitVerElem.style.display = 'none';
            $commitVerElem.value = '';
            updateVersion();
          });
    }
  }
}

function handleInputChange() {
  handleChange($inputPanes, $inputTypeElem.value);
}

function handleOutputChange() {
  $activeOutputElem = handleChange($panes, $outputTypeElem.value);
  updateLink();
}

function handleChange(panes, visiblePane) {
  var active = null;
  for (var i = 0; i < panes.length; i++) {
    if (panes[i].id === visiblePane) {
      panes[i].style.display = '';
      active = panes[i];
    } else {
      panes[i].style.display = 'none';
    }
  }
  return active;
};

function addCommitVersion(value, text, commit) {
  if (markedVersions[value]) {
    return;
  }
  markedVersions[value] = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + commit + '/lib/marked.js';
  var opt = document.createElement('option');
  opt.textContent = text;
  opt.value = value;
  $markedVerElem.insertBefore(opt, $markedVerElem.firstChild);
}

function getPrCommit(pr) {
  return fetch('https://api.github.com/repos/markedjs/marked/pulls/' + pr + '/commits')
    .then(function (res) {
      return res.json();
    })
    .then(function (json) {
      return json[json.length - 1].sha;
    }).catch(function () {
      // return undefined
    });
}

function setDefaultOptions() {
  if (window.Worker) {
    messageWorker({
      task: 'defaults',
      version: markedVersions[$markedVerElem.value]
    });
  } else {
    var defaults = marked.getDefaults();
    setOptions(defaults);
  }
}

function setOptions(opts) {
  $optionsElem.value = JSON.stringify(
    opts,
    function (key, value) {
      if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
        return undefined;
      }
      return value;
    }, ' ');
}

function searchToObject() {
  // modified from https://stackoverflow.com/a/7090123/806777
  var pairs = location.search.slice(1).split('&');
  var obj = {};

  for (var i = 0; i < pairs.length; i++) {
    if (pairs[i] === '') {
      continue;
    }

    var pair = pairs[i].split('=');

    obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
  }

  return obj;
}

function jsonString(input) {
  var output = (input + '')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r')
    .replace(/\t/g, '\\t')
    .replace(/\f/g, '\\f')
    .replace(/[\\"']/g, '\\$&')
    .replace(/\u0000/g, '\\0');
  return '"' + output + '"';
};

function getScrollSize() {
  var e = $activeOutputElem;

  return e.scrollHeight - e.clientHeight;
};

function getScrollPercent() {
  var size = getScrollSize();

  if (size <= 0) {
    return 1;
  }

  return $activeOutputElem.scrollTop / size;
};

function setScrollPercent(percent) {
  $activeOutputElem.scrollTop = percent * getScrollSize();
};

function updateLink() {
  var outputType = '';
  if ($outputTypeElem.value !== 'preview') {
    outputType = 'outputType=' + $outputTypeElem.value + '&';
  }

  $permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
      + '&options=' + encodeURIComponent($optionsElem.value)
      + '&version=' + encodeURIComponent($markedVerElem.value);
  history.replaceState('', document.title, $permalinkElem.href);
}

function updateVersion() {
  if (window.Worker) {
    handleInput();
    return Promise.resolve();
  }
  var promise;
  if (markedVersionCache[$markedVerElem.value]) {
    promise = Promise.resolve(markedVersionCache[$markedVerElem.value]);
  } else {
    promise = fetch(markedVersions[$markedVerElem.value])
      .then(function (res) { return res.text(); })
      .then(function (text) {
        markedVersionCache[$markedVerElem.value] = text;
        return text;
      });
  }
  return promise.then(function (text) {
    var script = document.createElement('script');
    script.textContent = text;

    $markedVer.parentNode.replaceChild(script, $markedVer);
    $markedVer = script;
  }).then(handleInput);
}

function checkForChanges() {
  if (inputDirty && $markedVerElem.value !== 'commit' && $markedVerElem.value !== 'pr' && (typeof marked !== 'undefined' || window.Worker)) {
    inputDirty = false;

    updateLink();

    var options = {};
    var optionsString = $optionsElem.value || '{}';
    try {
      var newOptions = JSON.parse(optionsString);
      options = newOptions;
      $optionsElem.classList.remove('error');
    } catch (err) {
      $optionsElem.classList.add('error');
    }

    var version = markedVersions[$markedVerElem.value];
    var markdown = $markdownElem.value;
    var hash = version + markdown + optionsString;
    if (lastInput !== hash) {
      lastInput = hash;
      if (window.Worker) {
        delayTime = 100;
        messageWorker({
          task: 'parse',
          version: version,
          markdown: markdown,
          options: options
        });
      } else {
        var startTime = new Date();
        var lexed = marked.lexer(markdown, options);
        var lexedList = [];
        for (var i = 0; i < lexed.length; i++) {
          var lexedLine = [];
          for (var j in lexed[i]) {
            lexedLine.push(j + ':' + jsonString(lexed[i][j]));
          }
          lexedList.push('{' + lexedLine.join(', ') + '}');
        }
        var parsed = marked.parser(lexed, options);
        var scrollPercent = getScrollPercent();
        setParsed(parsed, lexedList.join('\n'));
        setScrollPercent(scrollPercent);
        var endTime = new Date();
        delayTime = endTime - startTime;
        setResponseTime(delayTime);
        if (delayTime < 50) {
          delayTime = 50;
        } else if (delayTime > 500) {
          delayTime = 1000;
        }
      }
    }
  }
  checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
};

function setResponseTime(ms) {
  var amount = ms;
  var suffix = 'ms';
  if (ms > 1000 * 60 * 60) {
    amount = 'Too Long';
    suffix = '';
  } else if (ms > 1000 * 60) {
    amount = '>' + Math.floor(ms / (1000 * 60));
    suffix = 'm';
  } else if (ms > 1000) {
    amount = '>' + Math.floor(ms / 1000);
    suffix = 's';
  }
  $responseTimeElem.textContent = amount + suffix;
}

function setParsed(parsed, lexed) {
  try {
    $previewIframe.contentDocument.body.innerHTML = parsed;
  } catch (ex) {}
  $htmlElem.value = parsed;
  $lexerElem.value = lexed;
}

function messageWorker(message) {
  if (!markedWorker || markedWorker.working) {
    if (markedWorker) {
      clearTimeout(markedWorker.timeout);
      markedWorker.terminate();
    }
    markedWorker = new Worker('worker.js');
    markedWorker.onmessage = function (e) {
      clearTimeout(markedWorker.timeout);
      markedWorker.working = false;
      switch (e.data.task) {
        case 'defaults':
          setOptions(e.data.defaults);
          break;
        case 'parse':
          $previewElem.classList.remove('error');
          $htmlElem.classList.remove('error');
          $lexerElem.classList.remove('error');
          var scrollPercent = getScrollPercent();
          setParsed(e.data.parsed, e.data.lexed);
          setScrollPercent(scrollPercent);
          setResponseTime(e.data.time);
          break;
      }
      clearTimeout(checkChangeTimeout);
      delayTime = 10;
      checkForChanges();
    };
    markedWorker.onerror = markedWorker.onmessageerror = function (err) {
      clearTimeout(markedWorker.timeout);
      var error = 'There was an error in the Worker';
      if (err) {
        if (err.message) {
          error = err.message;
        } else {
          error = err;
        }
      }
      error = error.replace(/^Uncaught Error: /, '');
      $previewElem.classList.add('error');
      $htmlElem.classList.add('error');
      $lexerElem.classList.add('error');
      setParsed(error, error);
      setScrollPercent(0);
    };
  }
  if (message.task !== 'defaults') {
    markedWorker.working = true;
    workerTimeout(0);
  }
  markedWorker.postMessage(message);
}

function workerTimeout(seconds) {
  markedWorker.timeout = setTimeout(function () {
    seconds++;
    markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
    workerTimeout(seconds);
  }, 1000);
}
<|endoftext|>


<commit_before>'use strict';

const EventEmitter = require('events').EventEmitter;
const uuid = require('uuid/v4');
const redisKeys = require('./redis-keys');
const redisClient = require('./redis-client');
const heartBeat = require('./heartbeat');
const statsFactory = require('./stats');
const garbageCollector = require('./gc');
const logger = require('./logger');
const queue = require('./queue');

const CONSUMER_STATUS_GOING_DOWN = 0;
const CONSUMER_STATUS_DOWN = 1;
const CONSUMER_STATUS_GOING_UP = 2;
const CONSUMER_STATUS_UP = 3;
const CONSUMER_STATUS_CONSUMING = 4;

const sRegisterEventHandlers = Symbol('registerEventHandlers');
const sRegisterConsumerQueues = Symbol('registerConsumerQueues');
const sGetEventsHandlers = Symbol('getEventsHandlers');
const sGetNextMessage = Symbol('getNextMessage');
const sConsumeMessage = Symbol('consumeMessage');
const sProcessMessageFailure = Symbol('processMessageFailure');
const sHeartBeat = Symbol('heartBeat');
const sGarbageCollector = Symbol('garbageCollector');
const sStats = Symbol('stats');
const sLogger = Symbol('logger');
const sRedisClient = Symbol('redisClient');

class Consumer extends EventEmitter {

    /**
     * See docs.
     *
     * @param {object} config
     * @param {object} options
     */
    constructor(config = {}, options = {}) {
        super();

        if (!this.constructor.hasOwnProperty('queueName')) throw new Error('Undefined queue name!');

        /**
         * Event handlers
         */
        this[sRegisterEventHandlers]();

        /**
         * Consumer parameters
         */
        this.consumerId = uuid();
        this.queueName = redisKeys.validateKeyPart(this.constructor.queueName);
        this.config = config;
        this.options = options;
        this.messageConsumeTimeout = options.hasOwnProperty('messageConsumeTimeout') ?
            Number(options.messageConsumeTimeout) : 0;
        this.messageTTL = options.hasOwnProperty('messageTTL') ? Number(options.messageTTL) : 0;
        this.keys = redisKeys.getKeys(this);
        this.isTest = process.env.NODE_ENV === 'test';
        this.status = CONSUMER_STATUS_DOWN;

        /**
         * Project namespace.
         * If not provided the default one is used.
         */
        if (config.hasOwnProperty('namespace')) redisKeys.setNamespace(config.namespace);

        /**
         * Logs
         */
        this[sLogger] = logger.getNewInstance(`${this.queueName}:${this.consumerId}`, config.log);

        /**
         * Heartbeat
         */
        this[sHeartBeat] = heartBeat(this);

        /**
         * GC
         */
        this[sGarbageCollector] = garbageCollector(this, this[sLogger]);

        /**
         * Stats
         */
        const monitorEnabled = !!(config.monitor && config.monitor.enabled);
        if (monitorEnabled) this[sStats] = statsFactory(this, config);
    }

    /**
     *
     * @returns {object}
     */
    [sGetEventsHandlers]() {
        const consumer = this;
        const handlers = {
            /**
             *
             */
            halt() {
                consumer.status = CONSUMER_STATUS_DOWN;
                consumer.emit('halt');
            },

            /**
             * If an error occurred, the whole consumer should go down,
             * A consumer can not exit without heartbeat/gc and vice-versa
             */
            onError(err) {
                if (err.name !== 'AbortError' && consumer.status !== CONSUMER_STATUS_GOING_DOWN) {
                    consumer[sLogger].error(err);
                    process.exit(1);
                }
            },

            /**
             *
             */
            onHeartBeatHalt() {
                if (consumer[sStats]) consumer[sStats].stop();
                else handlers.halt();
            },

            /**
             *
             */
            onConsumerHalt() {
                consumer.status = CONSUMER_STATUS_GOING_DOWN;
                consumer[sRedisClient].end(true);
                delete consumer[sRedisClient];
                consumer[sGarbageCollector].stop();
            },

            /**
             *
             */
            onGCHalt() {
                consumer[sHeartBeat].stop();
            },

            /**
             *
             * @param message
             */
            onConsumeTimeout(message) {
                consumer[sProcessMessageFailure](
                    message, new Error(`Consumer timed out after [${consumer.messageConsumeTimeout}]`));
            },

            /**
             *
             * @param message
             */
            onMessageExpired(message) {
                consumer[sLogger].info(`Message [${message.uuid}] has expired`);
                consumer[sGarbageCollector].collectExpiredMessage(
                    message,
                    consumer.keys.keyQueueNameProcessing,
                    () => {
                        if (consumer[sStats]) consumer[sStats].incrementAcknowledgedSlot();
                        consumer[sLogger].info(`Message [${message.uuid}] successfully processed`);
                        consumer.emit('next');
                    });
            },

            /**
             *
             * @param message
             */
            onMessage(message) {
                if (consumer.status === CONSUMER_STATUS_UP) {
                    if (consumer[sGarbageCollector].checkMessageExpiration(message)) {
                        consumer.emit('message_expired', message);
                    } else consumer[sConsumeMessage](message);
                }
            },

            /**
             *
             */
            onNext() {
                if (consumer.isRunning()) {
                    consumer[sGetNextMessage]();
                }
            },

            /**
             *
             * @param message
             */
            onMessageConsumed(message) {
                handlers.onNext();
            },
        };
        return handlers;
    }

    /**
     *
     */
    [sRegisterEventHandlers]() {
        const handlers = this[sGetEventsHandlers]();

        /**
         * Events
         */
        this
        .on('next', handlers.onNext)
        .on('message', handlers.onMessage)
        .on('message_expired', handlers.onMessageExpired)
        .on('message_consumed', handlers.onMessageConsumed)
        .on('consume_timeout', handlers.onConsumeTimeout)
        .on('consumer_halt', handlers.onConsumerHalt)
        .on('gc_halt', handlers.onGCHalt)
        .on('heartbeat_halt', handlers.onHeartBeatHalt)
        .on('stats_halt', handlers.halt)
        .on('error', handlers.onError);
    }

    /**
     *
     * @param {function} cb
     */
    [sRegisterConsumerQueues](cb) {
        const messageQueue = () => {
            queue.addMessageQueue(this[sRedisClient], this.keys.keyQueueName, (err) => {
                if (err) cb(err);
                else processingQueue();
            });
        };

        const processingQueue = () => {
            queue.addProcessingQueue(this[sRedisClient], this.keys.keyQueueNameProcessing, (err) => {
                if (err) cb(err);
                else dlQueue();
            });
        };

        const dlQueue = () => {
            queue.addDLQueue(this[sRedisClient], this.keys.keyQueueNameDead, (err) => {
                if (err) cb(err);
                else cb();
            });
        };

        messageQueue();
    }

    /**
     *
     * @param {object} message
     * @param {object} error
     */
    [sProcessMessageFailure](message, error) {
        this[sLogger].error(`Consumer failed to consume message [${message.uuid}]...`);
        if (this[sStats]) this[sStats].incrementUnacknowledgedSlot();
        this[sGarbageCollector].collectMessage(message, this.keys.keyQueueNameProcessing, error, (err) => {
            if (err) this.emit('error', err);
            else this.emit('next');
        });
    }

    /**
     *
     */
    [sGetNextMessage]() {
        if (this.status !== CONSUMER_STATUS_UP) this.status = CONSUMER_STATUS_UP;
        this[sLogger].info('Waiting for new messages...');
        this[sRedisClient].brpoplpush(this.keys.keyQueueName, this.keys.keyQueueNameProcessing, 0, (err, payload) => {
            if (err) this.emit('error', err);
            else {
                this[sLogger].info('Got new message...');
                if (this[sStats]) this[sStats].incrementProcessingSlot();
                const message = JSON.parse(payload);
                this.emit('message', message);
            }
        });
    }

    /**
     *
     * @param {object} message
     */
    [sConsumeMessage](message) {
        this.status = CONSUMER_STATUS_CONSUMING;
        let isTimeout = false;
        let timer = null;
        this[sLogger].info(`Processing message [${message.uuid}]...`);
        try {
            if (this.messageConsumeTimeout) {
                timer = setTimeout(() => {
                    isTimeout = true;
                    timer = null;
                    this.emit('consume_timeout', message);
                }, this.messageConsumeTimeout);
            }
            const onDeleted = (err) => {
                if (err) this.emit('error', err);
                else {
                    if (this[sStats]) this[sStats].incrementAcknowledgedSlot();
                    this[sLogger].info(`Message [${message.uuid}] successfully processed`);
                    this.emit('message_consumed', message);
                }
            };
            const onConsumed = (err) => {
                if (this.isRunning() && !isTimeout) {
                    if (timer) clearTimeout(timer);
                    if (err) this[sProcessMessageFailure](message, err);
                    else this[sRedisClient].rpop(this.keys.keyQueueNameProcessing, onDeleted);
                }
            };
            this.consume(message.data, onConsumed);
        } catch (error) {
            this[sProcessMessageFailure](message, error);
        }
    }

    /**
     *
     */
    run() {
        if (this.status === CONSUMER_STATUS_DOWN) {
            this.status = CONSUMER_STATUS_GOING_UP;

            /**
             * Start heartbeat
             */
            this[sHeartBeat].start();

            /**
             * Start garbage collector
             */
            this[sGarbageCollector].start();

            /**
             * Start stats
             */
            if (this[sStats]) this[sStats].start();

            /**
             * Get a new Redis instance
             */
            this[sRedisClient] = redisClient.getNewInstance(this.config);

            /**
             * Register consumer queues
             */
            this[sRegisterConsumerQueues]((err) => {
                if (err) this.emit('error', err);
                else {
                    /**
                     * Wait for messages
                     */
                    this.emit('next');
                }
            });
        }
    }

    /**
     *
     */
    stop() {
        if (this.isRunning()) this.emit('consumer_halt');
    }

    /**
     *
     * @returns {boolean}
     */
    isRunning() {
        return ([CONSUMER_STATUS_GOING_DOWN, CONSUMER_STATUS_DOWN].indexOf(this.status) === -1);
    }

    /**
     *
     * @param {*} message
     * @param {function} cb
     */
    consume(message, cb) {
        /* eslint class-methods-use-this: 0 */
        throw new Error('Consume method should be extended');
    }
}

module.exports = Consumer;
<commit_msg>Fix message queues namespace bug (see issue #12)<commit_after>'use strict';

const EventEmitter = require('events').EventEmitter;
const uuid = require('uuid/v4');
const redisKeys = require('./redis-keys');
const redisClient = require('./redis-client');
const heartBeat = require('./heartbeat');
const statsFactory = require('./stats');
const garbageCollector = require('./gc');
const logger = require('./logger');
const queue = require('./queue');

const CONSUMER_STATUS_GOING_DOWN = 0;
const CONSUMER_STATUS_DOWN = 1;
const CONSUMER_STATUS_GOING_UP = 2;
const CONSUMER_STATUS_UP = 3;
const CONSUMER_STATUS_CONSUMING = 4;

const sRegisterEventHandlers = Symbol('registerEventHandlers');
const sRegisterConsumerQueues = Symbol('registerConsumerQueues');
const sGetEventsHandlers = Symbol('getEventsHandlers');
const sGetNextMessage = Symbol('getNextMessage');
const sConsumeMessage = Symbol('consumeMessage');
const sProcessMessageFailure = Symbol('processMessageFailure');
const sHeartBeat = Symbol('heartBeat');
const sGarbageCollector = Symbol('garbageCollector');
const sStats = Symbol('stats');
const sLogger = Symbol('logger');
const sRedisClient = Symbol('redisClient');

class Consumer extends EventEmitter {

    /**
     * See docs.
     *
     * @param {object} config
     * @param {object} options
     */
    constructor(config = {}, options = {}) {
        super();

        if (!this.constructor.hasOwnProperty('queueName')) throw new Error('Undefined queue name!');

        /**
         * Keys namespace.
         * If not provided the default one is used.
         */
        if (config.hasOwnProperty('namespace')) redisKeys.setNamespace(config.namespace);

        /**
         * Event handlers
         */
        this[sRegisterEventHandlers]();

        /**
         * Consumer parameters
         */
        this.consumerId = uuid();
        this.queueName = redisKeys.validateKeyPart(this.constructor.queueName);
        this.config = config;
        this.options = options;
        this.messageConsumeTimeout = options.hasOwnProperty('messageConsumeTimeout') ?
            Number(options.messageConsumeTimeout) : 0;
        this.messageTTL = options.hasOwnProperty('messageTTL') ? Number(options.messageTTL) : 0;
        this.keys = redisKeys.getKeys(this);
        this.isTest = process.env.NODE_ENV === 'test';
        this.status = CONSUMER_STATUS_DOWN;

        /**
         * Logs
         */
        this[sLogger] = logger.getNewInstance(`${this.queueName}:${this.consumerId}`, config.log);

        /**
         * Heartbeat
         */
        this[sHeartBeat] = heartBeat(this);

        /**
         * GC
         */
        this[sGarbageCollector] = garbageCollector(this, this[sLogger]);

        /**
         * Stats
         */
        const monitorEnabled = !!(config.monitor && config.monitor.enabled);
        if (monitorEnabled) this[sStats] = statsFactory(this, config);
    }

    /**
     *
     * @returns {object}
     */
    [sGetEventsHandlers]() {
        const consumer = this;
        const handlers = {
            /**
             *
             */
            halt() {
                consumer.status = CONSUMER_STATUS_DOWN;
                consumer.emit('halt');
            },

            /**
             * If an error occurred, the whole consumer should go down,
             * A consumer can not exit without heartbeat/gc and vice-versa
             */
            onError(err) {
                if (err.name !== 'AbortError' && consumer.status !== CONSUMER_STATUS_GOING_DOWN) {
                    consumer[sLogger].error(err);
                    process.exit(1);
                }
            },

            /**
             *
             */
            onHeartBeatHalt() {
                if (consumer[sStats]) consumer[sStats].stop();
                else handlers.halt();
            },

            /**
             *
             */
            onConsumerHalt() {
                consumer.status = CONSUMER_STATUS_GOING_DOWN;
                consumer[sRedisClient].end(true);
                delete consumer[sRedisClient];
                consumer[sGarbageCollector].stop();
            },

            /**
             *
             */
            onGCHalt() {
                consumer[sHeartBeat].stop();
            },

            /**
             *
             * @param message
             */
            onConsumeTimeout(message) {
                consumer[sProcessMessageFailure](
                    message, new Error(`Consumer timed out after [${consumer.messageConsumeTimeout}]`));
            },

            /**
             *
             * @param message
             */
            onMessageExpired(message) {
                consumer[sLogger].info(`Message [${message.uuid}] has expired`);
                consumer[sGarbageCollector].collectExpiredMessage(
                    message,
                    consumer.keys.keyQueueNameProcessing,
                    () => {
                        if (consumer[sStats]) consumer[sStats].incrementAcknowledgedSlot();
                        consumer[sLogger].info(`Message [${message.uuid}] successfully processed`);
                        consumer.emit('next');
                    });
            },

            /**
             *
             * @param message
             */
            onMessage(message) {
                if (consumer.status === CONSUMER_STATUS_UP) {
                    if (consumer[sGarbageCollector].checkMessageExpiration(message)) {
                        consumer.emit('message_expired', message);
                    } else consumer[sConsumeMessage](message);
                }
            },

            /**
             *
             */
            onNext() {
                if (consumer.isRunning()) {
                    consumer[sGetNextMessage]();
                }
            },

            /**
             *
             * @param message
             */
            onMessageConsumed(message) {
                handlers.onNext();
            },
        };
        return handlers;
    }

    /**
     *
     */
    [sRegisterEventHandlers]() {
        const handlers = this[sGetEventsHandlers]();

        /**
         * Events
         */
        this
        .on('next', handlers.onNext)
        .on('message', handlers.onMessage)
        .on('message_expired', handlers.onMessageExpired)
        .on('message_consumed', handlers.onMessageConsumed)
        .on('consume_timeout', handlers.onConsumeTimeout)
        .on('consumer_halt', handlers.onConsumerHalt)
        .on('gc_halt', handlers.onGCHalt)
        .on('heartbeat_halt', handlers.onHeartBeatHalt)
        .on('stats_halt', handlers.halt)
        .on('error', handlers.onError);
    }

    /**
     *
     * @param {function} cb
     */
    [sRegisterConsumerQueues](cb) {
        const messageQueue = () => {
            queue.addMessageQueue(this[sRedisClient], this.keys.keyQueueName, (err) => {
                if (err) cb(err);
                else processingQueue();
            });
        };

        const processingQueue = () => {
            queue.addProcessingQueue(this[sRedisClient], this.keys.keyQueueNameProcessing, (err) => {
                if (err) cb(err);
                else dlQueue();
            });
        };

        const dlQueue = () => {
            queue.addDLQueue(this[sRedisClient], this.keys.keyQueueNameDead, (err) => {
                if (err) cb(err);
                else cb();
            });
        };

        messageQueue();
    }

    /**
     *
     * @param {object} message
     * @param {object} error
     */
    [sProcessMessageFailure](message, error) {
        this[sLogger].error(`Consumer failed to consume message [${message.uuid}]...`);
        if (this[sStats]) this[sStats].incrementUnacknowledgedSlot();
        this[sGarbageCollector].collectMessage(message, this.keys.keyQueueNameProcessing, error, (err) => {
            if (err) this.emit('error', err);
            else this.emit('next');
        });
    }

    /**
     *
     */
    [sGetNextMessage]() {
        if (this.status !== CONSUMER_STATUS_UP) this.status = CONSUMER_STATUS_UP;
        this[sLogger].info('Waiting for new messages...');
        this[sRedisClient].brpoplpush(this.keys.keyQueueName, this.keys.keyQueueNameProcessing, 0, (err, payload) => {
            if (err) this.emit('error', err);
            else {
                this[sLogger].info('Got new message...');
                if (this[sStats]) this[sStats].incrementProcessingSlot();
                const message = JSON.parse(payload);
                this.emit('message', message);
            }
        });
    }

    /**
     *
     * @param {object} message
     */
    [sConsumeMessage](message) {
        this.status = CONSUMER_STATUS_CONSUMING;
        let isTimeout = false;
        let timer = null;
        this[sLogger].info(`Processing message [${message.uuid}]...`);
        try {
            if (this.messageConsumeTimeout) {
                timer = setTimeout(() => {
                    isTimeout = true;
                    timer = null;
                    this.emit('consume_timeout', message);
                }, this.messageConsumeTimeout);
            }
            const onDeleted = (err) => {
                if (err) this.emit('error', err);
                else {
                    if (this[sStats]) this[sStats].incrementAcknowledgedSlot();
                    this[sLogger].info(`Message [${message.uuid}] successfully processed`);
                    this.emit('message_consumed', message);
                }
            };
            const onConsumed = (err) => {
                if (this.isRunning() && !isTimeout) {
                    if (timer) clearTimeout(timer);
                    if (err) this[sProcessMessageFailure](message, err);
                    else this[sRedisClient].rpop(this.keys.keyQueueNameProcessing, onDeleted);
                }
            };
            this.consume(message.data, onConsumed);
        } catch (error) {
            this[sProcessMessageFailure](message, error);
        }
    }

    /**
     *
     */
    run() {
        if (this.status === CONSUMER_STATUS_DOWN) {
            this.status = CONSUMER_STATUS_GOING_UP;

            /**
             * Start heartbeat
             */
            this[sHeartBeat].start();

            /**
             * Start garbage collector
             */
            this[sGarbageCollector].start();

            /**
             * Start stats
             */
            if (this[sStats]) this[sStats].start();

            /**
             * Get a new Redis instance
             */
            this[sRedisClient] = redisClient.getNewInstance(this.config);

            /**
             * Register consumer queues
             */
            this[sRegisterConsumerQueues]((err) => {
                if (err) this.emit('error', err);
                else {
                    /**
                     * Wait for messages
                     */
                    this.emit('next');
                }
            });
        }
    }

    /**
     *
     */
    stop() {
        if (this.isRunning()) this.emit('consumer_halt');
    }

    /**
     *
     * @returns {boolean}
     */
    isRunning() {
        return ([CONSUMER_STATUS_GOING_DOWN, CONSUMER_STATUS_DOWN].indexOf(this.status) === -1);
    }

    /**
     *
     * @param {*} message
     * @param {function} cb
     */
    consume(message, cb) {
        /* eslint class-methods-use-this: 0 */
        throw new Error('Consume method should be extended');
    }
}

module.exports = Consumer;
<|endoftext|>


<commit_before>/* @flow */

import {
  canPopActiveStack,
  getActiveParentRouteType,
} from './ReducerUtils';
import { Actions } from 'history';
import { createMemoryHistory } from 'react-router';
import {
  createRandomKey,
  DEFAULT_KEY_LENGTH,
} from './LocationUtils';
import { RouteTypes } from './RouteUtils';

const { STACK_ROUTE } = RouteTypes;

const {
  PUSH: HISTORY_PUSH,
  REPLACE: HISTORY_REPLACE,
} = Actions;

let routerState = {};

const useNavState = (createHistory: Function) => (options = {}) => {
  const {
    transitionTo: baseTransitionTo,
    ...history,
  } = createHistory(options);

  const pop = (n = -1) => {
    const {
      navigationState,
    } = routerState;

    if (!n || n > -1) {
      return false;
    }

    const prevLocation = canPopActiveStack(n, navigationState);

    if (prevLocation) {
      const stateKey = prevLocation.state.stateKey;
      const location = history.createLocation(history.createPath(prevLocation), HISTORY_PUSH);
      location.state = { stateKey };

      baseTransitionTo(location);
      return true;
    }

    return false;
  };

  const transitionTo = (nextLocation) => {
    const {
      location: currentLocation,
      routes: currentRoutes,
    } = routerState;

    const activeRouteType = getActiveParentRouteType(currentRoutes);

    // History API treats HISTORY_PUSH to current path like HISTORY_REPLACE to be consistent with
    // browser behavior. (mjackson/history/blob/v2.0.1/modules/createHistory.js#L126) This is not
    // reasonable when performing `router.pop()` on <StackRoute />. A unique stateKey is needed for
    // each `location` to:
    // 1. bust the default 'HISTORY_REPLACE' behavior,
    // 2. location objects that are passed to reducer during a `pop` needs a stateKey to be able to
    // use a previous state for the newly HISTORY_PUSH'ed pointer to an older scene,
    // 3. History needs a unique `location.key` for each location entry. So cannot be used as
    // stateKey.
    const location = nextLocation;
    const stateKey = history.createKey();
    location.state = { ...location.state, stateKey };

    // e.g `transitionTo` is invoked via transition hooks
    // TODO Normalize history behavior to remove $routerReplace
    if (location.action === HISTORY_REPLACE) {
      location.state = { ...location.state, $routerReplace: true };
    }

    if (location.action === HISTORY_PUSH) {
      const currentPath = history.createPath(currentLocation);
      const nextPath = history.createPath(location);
      const currentStateKey = currentLocation.state.stateKey;
      const nextStateKey = location.state.stateKey;

      if (currentPath === nextPath && currentStateKey !== nextStateKey) {
        if (activeRouteType !== STACK_ROUTE) {
          location.action = HISTORY_REPLACE;
        }
      }
    }

    baseTransitionTo(location);
  };

  const push = (input) =>
          transitionTo(history.createLocation(input, HISTORY_PUSH));

  const replace = (input) =>
          transitionTo(history.createLocation(input, HISTORY_REPLACE));

  const syncRouterState = (state) => {
    routerState = state;
  };

  return {
    ...history,
    transitionTo,
    pop,
    push,
    replace,
    syncRouterState,
  };
};

const stateKey = createRandomKey(DEFAULT_KEY_LENGTH);
const rootEntry = { pathname: '/', state: { stateKey } };
const entries = [rootEntry];
const nativeHistory = useNavState(createMemoryHistory)({
  entries,
  keyLength: DEFAULT_KEY_LENGTH,
});

export default nativeHistory;
<commit_msg>fix `routerState` handling in `nativeHistory`<commit_after>/* @flow */

import {
  canPopActiveStack,
  getActiveParentRouteType,
} from './ReducerUtils';
import { Actions } from 'history';
import { createMemoryHistory } from 'react-router';
import {
  createRandomKey,
  DEFAULT_KEY_LENGTH,
} from './LocationUtils';
import { RouteTypes } from './RouteUtils';

const { STACK_ROUTE } = RouteTypes;

const {
  PUSH: HISTORY_PUSH,
  REPLACE: HISTORY_REPLACE,
} = Actions;

let routerState = null;

const useNavState = (createHistory: Function) => (options = {}) => {
  const {
    transitionTo: baseTransitionTo,
    ...history,
  } = createHistory(options);

  const pop = (n = -1): boolean => {
    if (!routerState) {
      return false;
    }

    const {
      navigationState,
    } = routerState;

    if (!n || n > -1) {
      return false;
    }

    const prevLocation = canPopActiveStack(n, navigationState);

    if (prevLocation) {
      const stateKey = prevLocation.state.stateKey;
      const location = history.createLocation(history.createPath(prevLocation), HISTORY_PUSH);
      location.state = { stateKey };

      baseTransitionTo(location);
      return true;
    }

    return false;
  };

  const transitionTo = (nextLocation) => {
    // History API treats HISTORY_PUSH to current path like HISTORY_REPLACE to be consistent with
    // browser behavior. (mjackson/history/blob/v2.0.1/modules/createHistory.js#L126) This is not
    // reasonable when performing `router.pop()` on <StackRoute />. A unique stateKey is needed for
    // each `location` to:
    // 1. bust the default 'HISTORY_REPLACE' behavior,
    // 2. location objects that are passed to reducer during a `pop` needs a stateKey to be able to
    // use a previous state for the newly HISTORY_PUSH'ed pointer to an older scene,
    // 3. History needs a unique `location.key` for each location entry. So cannot be used as
    // stateKey.
    const location = nextLocation;
    const stateKey = history.createKey();
    location.state = { ...location.state, stateKey };

    // e.g `transitionTo` is invoked via transition hooks
    // TODO Normalize history behavior to remove $routerReplace
    if (location.action === HISTORY_REPLACE) {
      location.state = { ...location.state, $routerReplace: true };
    }

    if (routerState && location.action === HISTORY_PUSH) {
      const {
        location: currentLocation,
        routes: currentRoutes,
      } = routerState;

      const currentPath = history.createPath(currentLocation);
      const nextPath = history.createPath(location);
      const currentStateKey = currentLocation.state.stateKey;
      const nextStateKey = location.state.stateKey;

      if (currentPath === nextPath && currentStateKey !== nextStateKey) {
        const activeRouteType = getActiveParentRouteType(currentRoutes);

        if (activeRouteType !== STACK_ROUTE) {
          location.action = HISTORY_REPLACE;
        }
      }
    }

    baseTransitionTo(location);
  };

  const push = (input) =>
          transitionTo(history.createLocation(input, HISTORY_PUSH));

  const replace = (input) =>
          transitionTo(history.createLocation(input, HISTORY_REPLACE));

  const syncRouterState = (state) => {
    routerState = state;
  };

  return {
    ...history,
    transitionTo,
    pop,
    push,
    replace,
    syncRouterState,
  };
};

const stateKey = createRandomKey(DEFAULT_KEY_LENGTH);
const rootEntry = { pathname: '/', state: { stateKey } };
const entries = [rootEntry];
const nativeHistory = useNavState(createMemoryHistory)({
  entries,
  keyLength: DEFAULT_KEY_LENGTH,
});

export default nativeHistory;
<|endoftext|>


<commit_before>'use strict';

//====================================================================

var assign = require('lodash.assign');
var Bluebird = require('bluebird');
var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
var jsonRpc = require('json-rpc');
var makeError = require('make-error');
var MethodNotFound = require('json-rpc/errors').MethodNotFound;
var WebSocket = require('ws');

var createCollection = require('./collection');

//====================================================================

// Expose Bluebird for now to ease integration (e.g. with Angular.js).
exports.setScheduler = Bluebird.setScheduler;

//====================================================================

function makeDeferred() {
  var resolve, reject;
  var promise = new Bluebird(function (resolve_, reject_) {
    resolve = resolve_;
    reject = reject_;
  });

  return {
    promise: promise,
    reject: reject,
    resolve: resolve,
  };
}

function startsWith(string, target) {
  return (string.lastIndexOf(target, 0) === 0);
}

//====================================================================

function returnThis() {
  /* jshint validthis: true */

  return this;
}

// Returns an iterator to the Fibonacci sequence.
function fibonacci(start) {
  var prev = 0;
  var curr = start || 1;

  var iterator = {
    next: function () {
      var tmp = curr;
      curr += prev;
      prev = tmp;

      return {
        done: false,
        value: prev,
      };
    },
  };

  // Make the iterator a true iterable (ES6).
  if (typeof Symbol !== 'undefined') {
    iterator[Symbol.iterator] = returnThis;
  }

  return iterator;
}

//====================================================================

// Fix URL if necessary.
var URL_RE = /^(?:(?:http|ws)(s)?:\/\/)?(.*?)\/*(?:\/api\/)?$/;
function fixUrl(url) {
  var matches = URL_RE.exec(url);
  var isSecure = !!matches[1];
  var rest = matches[2];

  return [
    isSecure ? 'wss' : 'ws',
    '://',
    rest,
    '/api/',
  ].join('');
}
exports.fixUrl = fixUrl;

//====================================================================

function getCurrentUrl() {
  /* global window: false */

  if (typeof window === undefined) {
    throw new Error('cannot get current URL');
  }

  return window.location.host + window.location.pathname;
}

//====================================================================

var ConnectionLost = makeError('ConnectionLost');

// Low level interface to XO.
function Api(url) {
  // Super constructor.
  EventEmitter.call(this);

  // Fix the URL (ensure correct protocol and /api/ path).
  this._url = fixUrl(url || getCurrentUrl());

  // Will contains the WebSocket.
  this._socket = null;

  // The JSON-RPC server.
  var this_ = this;
  this._jsonRpc = jsonRpc.createServer(function (message) {
    if (message.type === 'notification') {
      this_.emit('notification', message);
    } else {
      // This object does not support requests.
      throw new MethodNotFound(message.method);
    }
  }).on('data', function (message) {
    this_._socket.send(JSON.stringify(message));
  });
}
inherits(Api, EventEmitter);

assign(Api.prototype, {
  close: function () {
    if (this._socket) {
      this._socket.close();
    }
  },

  connect: Bluebird.method(function () {
    if (this._socket) {
      return;
    }

    var deferred = makeDeferred();

    var opts = {};
    if (startsWith(this._url, 'wss')) {
      // Due to imperfect TLS implementation in XO-Server.
      opts.rejectUnauthorized = false;
    }
    var socket = this._socket = new WebSocket(this._url, '', opts);

    // Used to avoid binding listeners to this object.
    var this_ = this;

    // When the socket opens, send any queued requests.
    socket.addEventListener('open', function () {
      // Resolves the promise.
      deferred.resolve();

      this_.emit('connected');
    });

    socket.addEventListener('message', function (message) {
      this_._jsonRpc.write(message.data);
    });

    socket.addEventListener('close', function () {
      this_._socket = null;

      this_._jsonRpc.failPendingRequests(new ConnectionLost());

      // Only emit this event if connected before.
      if (deferred.promise.isFulfilled()) {
        this_.emit('disconnected');
      }
    });

    socket.addEventListener('error', function (error) {
      // Fails the connect promise if possible.
      deferred.reject(error);
    });

    return deferred.promise;
  }),

  call: function (method, params) {
    var jsonRpc = this._jsonRpc;

    return this.connect().then(function () {
      return jsonRpc.request(method, params);
    });
  },
});

exports.Api = Api;

//====================================================================

var objectsOptions = {
  indexes: [
    'ref',
    'type',
    'UUID',
  ],
  key: function (item) {
    return item.UUID || item.ref;
  },
};

// High level interface to Xo.
//
// Handle auto-reconnect, sign in & objects cache.
function Xo(opts) {
  var self = this;

  this._api = new Api(opts.url);
  this._auth = opts.auth;
  this._backOff = fibonacci(1e3);
  this.objects = createCollection(objectsOptions);
  this.status = 'disconnected';
  this.user = null;

  // Promise representing the connection status.
  this._connection = null;

  self._api.on('disconnected', function () {
    self._connection = null;
    self.status = 'disconnected';

    // Automatically reconnect.
    self.connect();
  });

  self._api.on('notification', function (notification) {
    if (notification.method !== 'all') {
      return;
    }

    var method = (
      notification.params.type === 'exit' ?
        'unset' :
        'set'
    ) + 'Multiple';

    self.objects[method](notification.params.items);
  });
}

function tryConnect() {
  /* jshint validthis: true */

  return this._api.connect().bind(this).catch(function () {
    var delay = this._backOff.next().value;
    return Bluebird.delay(delay).bind(this).then(tryConnect);
  });
}

function onSuccessfulConnection() {
  /* jshint validthis: true */

  // FIXME: session.signIn() should work with both token and password.
  return this._api.call(
    this._auth.token ?
      'session.signInWithToken' :
      'session.signInWithPassword',
     this._auth
  ).bind(this).then(function (user) {
    this.user = user;
    this.status = 'connected';

    this._api.call('xo.getAllObjects').bind(this).then(function (objects) {
      this.objects.clear();
      this.objects.setMultiple(objects);
    });
  });
}

function onFailedConnection() {
  /* jshint validthis: true */

  this.status = 'disconnected';
}

Xo.prototype.connect = function () {
  if (this._connection) {
    return this._connection;
  }

  this.status = 'connecting';
  this._connection = tryConnect.call(this).then(
    onSuccessfulConnection, onFailedConnection
  );
  return this._connection;
};

Xo.prototype.call = function (method, params) {
  // TODO: prevent session.*() from being because it may interfere
  // with this class session management.

  return this.connect().then(function () {
    var self = this;
    return this._api.call(method, params).catch(ConnectionLost, function () {
      // Retry automatically.
      return self.call(method, params);
    });
  });
};

exports.Xo = Xo;

//====================================================================

function createXo(opts) {
  return new Xo(opts);
}

exports = module.exports = assign(createXo, module.exports);
<commit_msg>Do not use assign for methods.<commit_after>'use strict';

//====================================================================

var assign = require('lodash.assign');
var Bluebird = require('bluebird');
var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
var jsonRpc = require('json-rpc');
var makeError = require('make-error');
var MethodNotFound = require('json-rpc/errors').MethodNotFound;
var WebSocket = require('ws');

var createCollection = require('./collection');

//====================================================================

// Expose Bluebird for now to ease integration (e.g. with Angular.js).
exports.setScheduler = Bluebird.setScheduler;

//====================================================================

function makeDeferred() {
  var resolve, reject;
  var promise = new Bluebird(function (resolve_, reject_) {
    resolve = resolve_;
    reject = reject_;
  });

  return {
    promise: promise,
    reject: reject,
    resolve: resolve,
  };
}

function startsWith(string, target) {
  return (string.lastIndexOf(target, 0) === 0);
}

//====================================================================

function returnThis() {
  /* jshint validthis: true */

  return this;
}

// Returns an iterator to the Fibonacci sequence.
function fibonacci(start) {
  var prev = 0;
  var curr = start || 1;

  var iterator = {
    next: function () {
      var tmp = curr;
      curr += prev;
      prev = tmp;

      return {
        done: false,
        value: prev,
      };
    },
  };

  // Make the iterator a true iterable (ES6).
  if (typeof Symbol !== 'undefined') {
    iterator[Symbol.iterator] = returnThis;
  }

  return iterator;
}

//====================================================================

// Fix URL if necessary.
var URL_RE = /^(?:(?:http|ws)(s)?:\/\/)?(.*?)\/*(?:\/api\/)?$/;
function fixUrl(url) {
  var matches = URL_RE.exec(url);
  var isSecure = !!matches[1];
  var rest = matches[2];

  return [
    isSecure ? 'wss' : 'ws',
    '://',
    rest,
    '/api/',
  ].join('');
}
exports.fixUrl = fixUrl;

//====================================================================

function getCurrentUrl() {
  /* global window: false */

  if (typeof window === undefined) {
    throw new Error('cannot get current URL');
  }

  return window.location.host + window.location.pathname;
}

//====================================================================

var ConnectionLost = makeError('ConnectionLost');

// Low level interface to XO.
function Api(url) {
  // Super constructor.
  EventEmitter.call(this);

  // Fix the URL (ensure correct protocol and /api/ path).
  this._url = fixUrl(url || getCurrentUrl());

  // Will contains the WebSocket.
  this._socket = null;

  // The JSON-RPC server.
  var this_ = this;
  this._jsonRpc = jsonRpc.createServer(function (message) {
    if (message.type === 'notification') {
      this_.emit('notification', message);
    } else {
      // This object does not support requests.
      throw new MethodNotFound(message.method);
    }
  }).on('data', function (message) {
    this_._socket.send(JSON.stringify(message));
  });
}
inherits(Api, EventEmitter);

Api.prototype.close = function () {
  if (this._socket) {
    this._socket.close();
  }
};

Api.prototype.connect = Bluebird.method(function () {
  if (this._socket) {
    return;
  }

  var deferred = makeDeferred();

  var opts = {};
  if (startsWith(this._url, 'wss')) {
    // Due to imperfect TLS implementation in XO-Server.
    opts.rejectUnauthorized = false;
  }
  var socket = this._socket = new WebSocket(this._url, '', opts);

  // Used to avoid binding listeners to this object.
  var this_ = this;

  // When the socket opens, send any queued requests.
  socket.addEventListener('open', function () {
    // Resolves the promise.
    deferred.resolve();

    this_.emit('connected');
  });

  socket.addEventListener('message', function (message) {
    this_._jsonRpc.write(message.data);
  });

  socket.addEventListener('close', function () {
    this_._socket = null;

    this_._jsonRpc.failPendingRequests(new ConnectionLost());

    // Only emit this event if connected before.
    if (deferred.promise.isFulfilled()) {
      this_.emit('disconnected');
    }
  });

  socket.addEventListener('error', function (error) {
    // Fails the connect promise if possible.
    deferred.reject(error);
  });

  return deferred.promise;
});

Api.prototype.call = function (method, params) {
  var jsonRpc = this._jsonRpc;

  return this.connect().then(function () {
    return jsonRpc.request(method, params);
  });
};

exports.Api = Api;

//====================================================================

var objectsOptions = {
  indexes: [
    'ref',
    'type',
    'UUID',
  ],
  key: function (item) {
    return item.UUID || item.ref;
  },
};

// High level interface to Xo.
//
// Handle auto-reconnect, sign in & objects cache.
function Xo(opts) {
  var self = this;

  this._api = new Api(opts.url);
  this._auth = opts.auth;
  this._backOff = fibonacci(1e3);
  this.objects = createCollection(objectsOptions);
  this.status = 'disconnected';
  this.user = null;

  // Promise representing the connection status.
  this._connection = null;

  self._api.on('disconnected', function () {
    self._connection = null;
    self.status = 'disconnected';

    // Automatically reconnect.
    self.connect();
  });

  self._api.on('notification', function (notification) {
    if (notification.method !== 'all') {
      return;
    }

    var method = (
      notification.params.type === 'exit' ?
        'unset' :
        'set'
    ) + 'Multiple';

    self.objects[method](notification.params.items);
  });
}

function tryConnect() {
  /* jshint validthis: true */

  return this._api.connect().bind(this).catch(function () {
    var delay = this._backOff.next().value;
    return Bluebird.delay(delay).bind(this).then(tryConnect);
  });
}

function onSuccessfulConnection() {
  /* jshint validthis: true */

  // FIXME: session.signIn() should work with both token and password.
  return this._api.call(
    this._auth.token ?
      'session.signInWithToken' :
      'session.signInWithPassword',
     this._auth
  ).bind(this).then(function (user) {
    this.user = user;
    this.status = 'connected';

    this._api.call('xo.getAllObjects').bind(this).then(function (objects) {
      this.objects.clear();
      this.objects.setMultiple(objects);
    });
  });
}

function onFailedConnection() {
  /* jshint validthis: true */

  this.status = 'disconnected';
}

Xo.prototype.connect = function () {
  if (this._connection) {
    return this._connection;
  }

  this.status = 'connecting';
  this._connection = tryConnect.call(this).then(
    onSuccessfulConnection, onFailedConnection
  );
  return this._connection;
};

Xo.prototype.call = function (method, params) {
  // TODO: prevent session.*() from being because it may interfere
  // with this class session management.

  return this.connect().then(function () {
    var self = this;
    return this._api.call(method, params).catch(ConnectionLost, function () {
      // Retry automatically.
      return self.call(method, params);
    });
  });
};

exports.Xo = Xo;

//====================================================================

function createXo(opts) {
  return new Xo(opts);
}

exports = module.exports = assign(createXo, module.exports);
<|endoftext|>


<commit_before>var colors = require('colors');
var fs = require('fs');
var prompt = require('prompt-sync');
var child_process = require('child_process');
var pkg = require('./package.json');
var oldVersion = pkg.version;
var newVersion;
var abort = [ 'git checkout master', 'rm abort push' ];
var push  = [ 'rm abort push'];
var defaultOptions = { encoding: 'utf-8' };

if (validate()) {
  newVersion = getNewVersion();

  console.log('\n--------\n');

  checkoutVersionBranch();
  updateVersion();
  createChangelog();
  commitChanges();
  tagRelease();
  cloneRepo('bower-material', 2);
  updateBowerVersion();
  cloneRepo('code.material.angularjs.org', 1);
  updateSite();
  updateMaster();
  writeScript('abort', abort);
  writeScript('push', push);

  console.log('\n--------\n');
  console.log('Your repo is ready to be pushed.');
  console.log('Please look over CHANGELOG.md and make any changes.');
  console.log('When you are ready, please run "./push" to finish the process.');
  console.log('If you would like to cancel this release, please run "./abort"');
}

//-- utility methods

function validate () {
  if (exec('npm whoami') !== 'angularcore') {
    console.log('You must be authenticated with npm as "angularcore" to perform a release.'.red);
  } else if (exec('git rev-parse --abbrev-ref HEAD') !== 'master') {
    console.log('Releases can only performed from master at this time.'.red);
  } else {
    return true;
  }
}

function checkoutVersionBranch () {
  exec(fill('git checkout -q -b release/{{newVersion}}'));
  abort.push(fill('git branch -D release/{{newVersion}}'));
}

function updateVersion () {
  process.stdout.write(fill('Updating package.json version from {{oldVersion}} to {{newVersion}}...'));
  pkg.version = newVersion;
  fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
  done();
  abort.push('git checkout package.json');
  push.push('git add package.json');
}

function createChangelog () {
  process.stdout.write(fill('Generating changelog from v{{oldVersion}} to v{{newVersion}}...'));
  var cmd = fill('gulp changelog --sha=$(git merge-base v{{oldVersion}} HEAD)');
  exec(cmd);
  done();
  abort.push('git checkout CHANGELOG.md');
  push.push('git add CHANGELOG.md');
}

function clear () {
  process.stdout.write("\u001b[2J\u001b[0;0H");
}

function getNewVersion () {
  clear();
  var options = getVersionOptions(oldVersion), key, type, version;
  console.log(fill('The current version is {{oldVersion}}.'));
  console.log('');
  console.log('What type of release is this?');
  for (key in options) { console.log((+key + 1) + ') ' + options[key]); }
  console.log('');
  process.stdout.write('Please select a new version: ');
  type = prompt();

  if (options[type - 1]) version = options[type - 1];
  else if (type.match(/^\d+\.\d+\.\d+(-rc\d+)?$/)) version = type;
  else throw new Error('Your entry was invalid.');

  if (version.indexOf('rc') < 0) {
    console.log('');
    process.stdout.write('Is this a release candidate? [yes/no] ');
    if (prompt() === 'yes') version += '-rc1';
  }

  console.log('');
  process.stdout.write('The new version will be ' + version + '.  Is this correct? [yes/no] ');
  return prompt() === 'yes' ? version : getNewVersion();

  function getVersionOptions (version) {
    return version.match(/-rc\d+$/)
        ? [ increment(version, 'rc'),
            increment(version, 'minor') ]
        : [ increment(version, 'patch'),
            increment(version, 'minor'),
            increment(version, 'major') ];

    function increment (versionString, type) {
      var version = parseVersion(versionString);
      if (version.rc) {
        switch (type) {
          case 'minor': version.rc = 0; break;
          case 'rc': version.rc++; break;
        }
      } else {
        version[type]++;
        //-- reset any version numbers lower than the one changed
        switch (type) {
          case 'major': version.minor = 0;
          case 'minor': version.patch = 0;
          case 'patch': version.rc = 0;
        }
      }
      return getVersionString(version);

      function parseVersion (version) {
        var parts = version.split(/\.|\-rc/g);
        return { string: version, major: parts[0], minor: parts[1], patch: parts[2], rc: parts[3] || 0 };
      }

      function getVersionString(version) {
        var str = version.major + '.' + version.minor + '.' + version.patch;
        if (version.rc) str += '-rc' + version.rc;
        return str;
      }
    };
  }
}

function tagRelease () {
  process.stdout.write('Tagging release...');
  exec(fill('git tag v{{newVersion}}'));
  done();
  abort.push(fill('git tag -d v{{newVersion}}'));
  push.push(
      fill('git push origin v{{newVersion}}'),
      fill('git push --set-upstream origin release/{{newVersion}}'),
      'npm publish'
  );
}

function commitChanges () {
  process.stdout.write('Committing changes...');
  exec(fill('git commit -am "release: version {{newVersion}}"'));
  done();
  push.push('git commit --amend --no-edit');
}

function cloneRepo (repo, depth) {
  depth = depth || 1;
  process.stdout.write('Cloning ' + repo + ' from Github...');
  exec('git clone https://github.com/angular/' + repo + '.git --depth=' + depth + ' 2> /dev/null');
  done();
  abort.push('rm -rf ' + repo);
}

function fill(str) {
  return str.replace(/\{\{[^\}]+\}\}/g, function (match) {
    return eval(match.substr(2, match.length - 4));
  });
}

function writeScript (name, cmds) {
  fs.writeFileSync(name, cmds.join('\n'));
  exec('chmod +x ' + name);
}

function updateBowerVersion () {
  process.stdout.write('Updating bower version...');
  var options = { cwd: './bower-material' },
      bower = require(options.cwd + '/bower.json'),
      package = require(options.cwd + '/package.json');
  //-- update versions in config files
  bower.version = package.version = newVersion;
  fs.writeFileSync(options.cwd + '/package.json', JSON.stringify(package, null, 2));
  fs.writeFileSync(options.cwd + '/bower.json', JSON.stringify(bower, null, 2));
  done();
  process.stdout.write('Building bower files...');
  //-- build files for bower
  exec('rm -rf dist');
  exec('gulp build');
  exec('gulp build --mode=default');
  exec('gulp build --mode=closure');
  done();
  process.stdout.write('Copy files into bower repo...');
  //-- copy files over to bower repo
  exec('cp -Rf ../dist/* ./', options);
  exec('git add -A', options);
  exec(fill('git commit -m "release: version {{newVersion}}"'), options);
  exec(fill('git tag -f v{{newVersion}}'), options);
  exec('rm -rf dist');
  done();
  //-- add steps to push script
  push.push(
      'cd ' + options.cwd,
      'git push -q origin master',
      fill('git push -q origin v{{newVersion}}'),
      'cd ..'
  );
}

function updateSite () {
  process.stdout.write('Adding new version of the docs site...');
  var options = { cwd: './code.material.angularjs.org' },
      config  = require(options.cwd + '/docs.json');
  config.versions.unshift(newVersion);
  config.latest = newVersion;
  fs.writeFileSync(options.cwd + '/docs.json', JSON.stringify(config, null, 2));
  //-- build files for bower
  exec('rm -rf dist');
  exec('gulp docs');
  //-- copy files over to site repo
  exec('rm -rf latest', options);
  exec(fill('cp -Rf ../dist/docs {{newVersion}}'), options);
  exec(fill('cp -Rf ../dist/docs latest'), options);
  exec('git add -A', options);
  exec(fill('git commit -m "release: version {{newVersion}}"'), options);
  exec(fill('git tag -f v{{newVersion}}'), options);
  exec('rm -rf dist');
  done();
  //-- add steps to push script
  push.push(
      'cd ' + options.cwd,
      'git push -q origin master',
      fill('git push -q origin v{{newVersion}}'),
      'cd ..'
  );
}

function updateMaster () {
  push.push(
      'git co master',
      'node -e "' + stringifyFunction(buildCommand) + '"',
      'git add package.json',
      fill('git commit -m "update version number in package.json to {{newVersion}}"')
  );
  function buildCommand () {
    require('fs').writeFileSync('package.json', JSON.stringify(getUpdatedJson(), null, 2));
    function getUpdatedJson () {
      var json = require('./package.json');
      json.version = '{{newVersion}}';
      return json;
    }
  }
  function stringifyFunction (method) {
    return fill(method
        .toString()
        .split('\n')
        .slice(1, -1)
        .map(function (line) { return line.trim(); })
        .join(' ')
        .replace(/\"/g, '\\\"'));
  }
}

function done () {
  console.log('done.'.green);
}

function exec (cmd, userOptions) {
  var options = Object.create(defaultOptions);
  for (var key in userOptions) options[key] = userOptions[key];
  return child_process.execSync(cmd, options).trim();
}
<commit_msg>update(build): moves npm publish to bower repo<commit_after>var colors = require('colors');
var fs = require('fs');
var prompt = require('prompt-sync');
var child_process = require('child_process');
var pkg = require('./package.json');
var oldVersion = pkg.version;
var newVersion;
var abort = [ 'git checkout master', 'rm abort push' ];
var push  = [ 'rm abort push'];
var defaultOptions = { encoding: 'utf-8' };

if (validate()) {
  newVersion = getNewVersion();

  console.log('\n--------\n');

  checkoutVersionBranch();
  updateVersion();
  createChangelog();
  commitChanges();
  tagRelease();
  cloneRepo('bower-material', 2);
  updateBowerVersion();
  cloneRepo('code.material.angularjs.org', 1);
  updateSite();
  updateMaster();
  writeScript('abort', abort);
  writeScript('push', push);

  console.log('\n--------\n');
  console.log('Your repo is ready to be pushed.');
  console.log('Please look over CHANGELOG.md and make any changes.');
  console.log('When you are ready, please run "./push" to finish the process.');
  console.log('If you would like to cancel this release, please run "./abort"');
}

//-- utility methods

function validate () {
  if (exec('npm whoami') !== 'angularcore') {
    console.log('You must be authenticated with npm as "angularcore" to perform a release.'.red);
  } else if (exec('git rev-parse --abbrev-ref HEAD') !== 'master') {
    console.log('Releases can only performed from master at this time.'.red);
  } else {
    return true;
  }
}

function checkoutVersionBranch () {
  exec(fill('git checkout -q -b release/{{newVersion}}'));
  abort.push(fill('git branch -D release/{{newVersion}}'));
}

function updateVersion () {
  process.stdout.write(fill('Updating package.json version from {{oldVersion}} to {{newVersion}}...'));
  pkg.version = newVersion;
  fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
  done();
  abort.push('git checkout package.json');
  push.push('git add package.json');
}

function createChangelog () {
  process.stdout.write(fill('Generating changelog from v{{oldVersion}} to v{{newVersion}}...'));
  var cmd = fill('gulp changelog --sha=$(git merge-base v{{oldVersion}} HEAD)');
  exec(cmd);
  done();
  abort.push('git checkout CHANGELOG.md');
  push.push('git add CHANGELOG.md');
}

function clear () {
  process.stdout.write("\u001b[2J\u001b[0;0H");
}

function getNewVersion () {
  clear();
  var options = getVersionOptions(oldVersion), key, type, version;
  console.log(fill('The current version is {{oldVersion}}.'));
  console.log('');
  console.log('What type of release is this?');
  for (key in options) { console.log((+key + 1) + ') ' + options[key]); }
  console.log('');
  process.stdout.write('Please select a new version: ');
  type = prompt();

  if (options[type - 1]) version = options[type - 1];
  else if (type.match(/^\d+\.\d+\.\d+(-rc\d+)?$/)) version = type;
  else throw new Error('Your entry was invalid.');

  if (version.indexOf('rc') < 0) {
    console.log('');
    process.stdout.write('Is this a release candidate? [yes/no] ');
    if (prompt() === 'yes') version += '-rc1';
  }

  console.log('');
  process.stdout.write('The new version will be ' + version + '.  Is this correct? [yes/no] ');
  return prompt() === 'yes' ? version : getNewVersion();

  function getVersionOptions (version) {
    return version.match(/-rc\d+$/)
        ? [ increment(version, 'rc'),
            increment(version, 'minor') ]
        : [ increment(version, 'patch'),
            increment(version, 'minor'),
            increment(version, 'major') ];

    function increment (versionString, type) {
      var version = parseVersion(versionString);
      if (version.rc) {
        switch (type) {
          case 'minor': version.rc = 0; break;
          case 'rc': version.rc++; break;
        }
      } else {
        version[type]++;
        //-- reset any version numbers lower than the one changed
        switch (type) {
          case 'major': version.minor = 0;
          case 'minor': version.patch = 0;
          case 'patch': version.rc = 0;
        }
      }
      return getVersionString(version);

      function parseVersion (version) {
        var parts = version.split(/\.|\-rc/g);
        return { string: version, major: parts[0], minor: parts[1], patch: parts[2], rc: parts[3] || 0 };
      }

      function getVersionString(version) {
        var str = version.major + '.' + version.minor + '.' + version.patch;
        if (version.rc) str += '-rc' + version.rc;
        return str;
      }
    };
  }
}

function tagRelease () {
  process.stdout.write('Tagging release...');
  exec(fill('git tag v{{newVersion}}'));
  done();
  abort.push(fill('git tag -d v{{newVersion}}'));
  push.push(
      fill('git push origin v{{newVersion}}'),
      fill('git push --set-upstream origin release/{{newVersion}}')
  );
}

function commitChanges () {
  process.stdout.write('Committing changes...');
  exec(fill('git commit -am "release: version {{newVersion}}"'));
  done();
  push.push('git commit --amend --no-edit');
}

function cloneRepo (repo, depth) {
  depth = depth || 1;
  process.stdout.write('Cloning ' + repo + ' from Github...');
  exec('git clone https://github.com/angular/' + repo + '.git --depth=' + depth + ' 2> /dev/null');
  done();
  abort.push('rm -rf ' + repo);
}

function fill(str) {
  return str.replace(/\{\{[^\}]+\}\}/g, function (match) {
    return eval(match.substr(2, match.length - 4));
  });
}

function writeScript (name, cmds) {
  fs.writeFileSync(name, cmds.join('\n'));
  exec('chmod +x ' + name);
}

function updateBowerVersion () {
  process.stdout.write('Updating bower version...');
  var options = { cwd: './bower-material' },
      bower = require(options.cwd + '/bower.json'),
      package = require(options.cwd + '/package.json');
  //-- update versions in config files
  bower.version = package.version = newVersion;
  fs.writeFileSync(options.cwd + '/package.json', JSON.stringify(package, null, 2));
  fs.writeFileSync(options.cwd + '/bower.json', JSON.stringify(bower, null, 2));
  done();
  process.stdout.write('Building bower files...');
  //-- build files for bower
  exec('rm -rf dist');
  exec('gulp build');
  exec('gulp build --mode=default');
  exec('gulp build --mode=closure');
  done();
  process.stdout.write('Copy files into bower repo...');
  //-- copy files over to bower repo
  exec('cp -Rf ../dist/* ./', options);
  exec('git add -A', options);
  exec(fill('git commit -m "release: version {{newVersion}}"'), options);
  exec(fill('git tag -f v{{newVersion}}'), options);
  exec('rm -rf dist');
  done();
  //-- add steps to push script
  push.push(
      'cd ' + options.cwd,
      'git push -q origin master',
      fill('git push -q origin v{{newVersion}}'),
      'npm publish',
      'cd ..'
  );
}

function updateSite () {
  process.stdout.write('Adding new version of the docs site...');
  var options = { cwd: './code.material.angularjs.org' },
      config  = require(options.cwd + '/docs.json');
  config.versions.unshift(newVersion);
  config.latest = newVersion;
  fs.writeFileSync(options.cwd + '/docs.json', JSON.stringify(config, null, 2));
  //-- build files for bower
  exec('rm -rf dist');
  exec('gulp docs');
  //-- copy files over to site repo
  exec('rm -rf latest', options);
  exec(fill('cp -Rf ../dist/docs {{newVersion}}'), options);
  exec(fill('cp -Rf ../dist/docs latest'), options);
  exec('git add -A', options);
  exec(fill('git commit -m "release: version {{newVersion}}"'), options);
  exec(fill('git tag -f v{{newVersion}}'), options);
  exec('rm -rf dist');
  done();
  //-- add steps to push script
  push.push(
      'cd ' + options.cwd,
      'git push -q origin master',
      fill('git push -q origin v{{newVersion}}'),
      'cd ..'
  );
}

function updateMaster () {
  push.push(
      'git co master',
      'node -e "' + stringifyFunction(buildCommand) + '"',
      'git add package.json',
      fill('git commit -m "update version number in package.json to {{newVersion}}"')
  );
  function buildCommand () {
    require('fs').writeFileSync('package.json', JSON.stringify(getUpdatedJson(), null, 2));
    function getUpdatedJson () {
      var json = require('./package.json');
      json.version = '{{newVersion}}';
      return json;
    }
  }
  function stringifyFunction (method) {
    return fill(method
        .toString()
        .split('\n')
        .slice(1, -1)
        .map(function (line) { return line.trim(); })
        .join(' ')
        .replace(/\"/g, '\\\"'));
  }
}

function done () {
  console.log('done.'.green);
}

function exec (cmd, userOptions) {
  var options = Object.create(defaultOptions);
  for (var key in userOptions) options[key] = userOptions[key];
  return child_process.execSync(cmd, options).trim();
}
<|endoftext|>