<commit_before>const searchEngine = require('util/searchEngine.js')

const hosts = require('./hosts.js')

var urlParser = {
  startingWWWRegex: /www\.(.+\..+\/)/g,
  trailingSlashRegex: /\/$/g,
  isURL: function (url) {
    return url.indexOf('http://') === 0 || url.indexOf('https://') === 0 || url.indexOf('file://') === 0 || url.indexOf('about:') === 0 || url.indexOf('chrome:') === 0 || url.indexOf('data:') === 0
  },
  removeProtocol: function (url) {
    if (!urlParser.isURL(url)) {
      return url
    }

    var withoutProtocol = url.replace('http://', '').replace('https://', '').replace('file://', '') // chrome:, about:, data: protocols intentionally not removed

    if (withoutProtocol.indexOf('www.') === 0) {
      return withoutProtocol.replace('www.', '')
    } else {
      return withoutProtocol
    }
  },
  isURLMissingProtocol: function (url) {
    if (url.indexOf(' ') === -1 && url.indexOf('.') > 0) {
      return true
    }
    var hostPart = url.replace(/(:|\/).+/, '')
    return hosts.indexOf(hostPart) > -1
  },
  parse: function (url) {
    url = url.trim() // remove whitespace common on copy-pasted url's

    if (!url) {
      return 'about:blank'
    }
    // if the url starts with a (supported) protocol, do nothing
    if (urlParser.isURL(url)) {
      return url
    }

    if (url.indexOf('view-source:') === 0) {
      var realURL = url.replace('view-source:', '')

      return 'view-source:' + urlParser.parse(realURL)
    }

    // if the url doesn't have a space and has a ., or is a host from hosts file, assume it is a url without a protocol
    if (urlParser.isURLMissingProtocol(url)) {
      return 'http://' + url
    }
    // else, do a search
    return searchEngine.getCurrent().searchURL.replace('%s', encodeURIComponent(url))
  },
  basicURL: function (url) {
    return urlParser.removeProtocol(url).replace(urlParser.trailingSlashRegex, '')
  },
  prettyURL: function (url) {
    try {
      var urlOBJ = new URL(url)
      return (urlOBJ.hostname + urlOBJ.pathname).replace(urlParser.startingWWWRegex, '$1').replace(urlParser.trailingSlashRegex, '')
    } catch (e) { // URL constructor will throw an error on malformed URLs
      return url
    }
  },
  isInternalURL: function (url) {
    return url.startsWith(urlParser.getFileURL(__dirname))
  },
  getSourceURL: function (url) {
    // converts internal URLs (like the PDF viewer or the reader view) to the URL of the page they are displaying
    if (urlParser.isInternalURL(url)) {
      try {
        var realURL = new URLSearchParams(new URL(url).search).get('url')
        if (realURL) {
          return realURL
        }
      } catch(e) {}
    }
    return url
  },
  getFileURL: function (path) {
    if (window.platformType === 'windows') {
      // convert backslash to forward slash
      path = path.replace(/\\/g, '/')
      // https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/

      // UNC path?
      if (path.startsWith('//')) {
        return 'file:' + path
      } else {
        return 'file:///' + path
      }
    } else {
      return 'file://' + path
    }
  }
}

module.exports = urlParser
<commit_msg>display internal pages as min: urls<commit_after>const searchEngine = require('util/searchEngine.js')

const hosts = require('./hosts.js')

var urlParser = {
  startingWWWRegex: /www\.(.+\..+\/)/g,
  trailingSlashRegex: /\/$/g,
  isURL: function (url) {
    return url.indexOf('http://') === 0 || url.indexOf('https://') === 0 || url.indexOf('file://') === 0 || url.indexOf('about:') === 0 || url.indexOf('chrome:') === 0 || url.indexOf('data:') === 0
  },
  removeProtocol: function (url) {
    if (!urlParser.isURL(url)) {
      return url
    }

    var withoutProtocol = url.replace('http://', '').replace('https://', '').replace('file://', '') // chrome:, about:, data: protocols intentionally not removed

    if (withoutProtocol.indexOf('www.') === 0) {
      return withoutProtocol.replace('www.', '')
    } else {
      return withoutProtocol
    }
  },
  isURLMissingProtocol: function (url) {
    // assume anything with no spaces and a . is a URL
    if (url.indexOf(' ') === -1 && url.indexOf('.') > 0) {
      return true
    }
    // a host from the hosts file is also a URL
    var hostPart = url.replace(/(:|\/).+/, '')
    return hosts.indexOf(hostPart) > -1
  },
  parse: function (url) {
    url = url.trim() // remove whitespace common on copy-pasted url's

    if (!url) {
      return 'about:blank'
    }

    if (url.indexOf('view-source:') === 0) {
      var realURL = url.replace('view-source:', '')

      return 'view-source:' + urlParser.parse(realURL)
    }

    // if the URL is an internal URL, convert it to the correct file:// url
    if (url.startsWith('min:')) {
      try {
        var urlObj = new URL(url)
        var path = urlObj.pathname.replace('//', '')
        if (/^[a-zA-Z]+$/.test(path)) {
          // only paths with letters are allowed
          return urlParser.getFileURL(__dirname + '/pages/' + path + '/index.html' + urlObj.search)
        }
      } catch (e) {}
    }

    // if the url starts with a (supported) protocol, do nothing
    if (urlParser.isURL(url)) {
      return url
    }

    // if the url doesn't have a space and has a ., or is a host from hosts file, assume it is a url without a protocol
    if (urlParser.isURLMissingProtocol(url)) {
      return 'http://' + url
    }
    // else, do a search
    return searchEngine.getCurrent().searchURL.replace('%s', encodeURIComponent(url))
  },
  basicURL: function (url) {
    return urlParser.removeProtocol(url).replace(urlParser.trailingSlashRegex, '')
  },
  prettyURL: function (url) {
    try {
      var urlOBJ = new URL(url)
      return (urlOBJ.hostname + urlOBJ.pathname).replace(urlParser.startingWWWRegex, '$1').replace(urlParser.trailingSlashRegex, '')
    } catch (e) { // URL constructor will throw an error on malformed URLs
      return url
    }
  },
  isInternalURL: function (url) {
    return url.startsWith(urlParser.getFileURL(__dirname))
  },
  getSourceURL: function (url) {
    // converts internal URLs (like the PDF viewer or the reader view) to the URL of the page they are displaying
    if (urlParser.isInternalURL(url)) {
      var representedURL
      try {
        representedURL = new URLSearchParams(new URL(url).search).get('url')
      } catch(e) {}
      if (representedURL) {
        return representedURL
      } else {
        try {
          var pageName = url.match(/\/pages\/([a-zA-Z]+)\//)
          var urlObj = new URL(url)
          if (pageName) {
            return 'min://' + pageName[1] + urlObj.search
          }
        } catch (e) {}
      }
    }
    return url
  },
  getFileURL: function (path) {
    if (window.platformType === 'windows') {
      // convert backslash to forward slash
      path = path.replace(/\\/g, '/')
      // https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/

      // UNC path?
      if (path.startsWith('//')) {
        return 'file:' + path
      } else {
        return 'file:///' + path
      }
    } else {
      return 'file://' + path
    }
  }
}

module.exports = urlParser
<|endoftext|>


<commit_before>
/*
 *  OpenWFEru - open source ruby workflow and bpm engine
 *  (c) 2008 John Mettraux
 *
 *  OpenWFEru is freely distributable under the terms
 *  of a BSD-style license.
 *  For details, see the OpenWFEru web site: http://openwferu.rubyforge.org
 *
 *  This piece of hack was created during the RubyKaigi2008,
 *  between Tsukuba and Akihabara.
 */

//var flow = [ 'process-definition', { 'name': 'toto', 'revision': '1.0' }, [
//    [ 'sequence', {}, [
//      [ 'participant', { 'ref': 'alpha' }, [] ],
//      [ 'bravo', {}, [] ]
//    ]]
//  ]
//]

HTMLElement.prototype.firstChildOfClass = function (className) {

  for (var i=0; i < this.childNodes.length; i++) {
    var c = this.childNodes[i];
    if (c.className == className) return c;
  }

  return null;
}
String.prototype.tstrip = function () {
  var s = this;
  while (s.charAt(0) == ' ') s = s.substring(1);
  while (s.charAt(s.length - 1) == ' ') s = s.substring(0, s.length - 1);
  return s;
}

var Tred = function () {

  var ExpressionHead = function () {

    function createButton (imgsrc, tooltip, callback) {

      var i = document.createElement("img");
      i.callback = callback;
      i.className = "tred_button";
      i.setAttribute('src', imgsrc);
      i.setAttribute('title', tooltip);
      i.setAttribute("onclick", "this.callback()");
      return i;
    }

    function addHeadButtons (expdiv) {

      var outOpacity = 0.0;

      var buttons = document.createElement("span");
      buttons.style.opacity = outOpacity;

      expdiv.onmouseover = function () { buttons.style.opacity = 1.0; }
      expdiv.onmouseout = function () { buttons.style.opacity = outOpacity; }

      buttons.appendChild(createButton(
        'images/btn-add.gif',
        'add a child expression',
        function () {
          Tred.addExpression(expdiv.parentNode, [ "---", {}, [] ]);
        }));
      buttons.appendChild(createButton(
        'images/btn-cut.gif',
        'cut expression',
        function () {
          Tred.removeExpression(expdiv.parentNode);
        }));

      buttons.appendChild(createButton(
        'images/btn-moveup.gif',
        'move expression up',
        function () {
          Tred.moveExpression(expdiv.parentNode, -1);
          buttons.style.opacity = outOpacity;
        }));

      buttons.appendChild(createButton(
        'images/btn-movedown.gif',
        'move expression down',
        function () {
          Tred.moveExpression(expdiv.parentNode, +1);
          buttons.style.opacity = outOpacity;
        }));

      buttons.appendChild(createButton(
        'images/btn-paste.gif',
        'paste expression here',
        function () {
          alert('please implement me');
        }));

      expdiv.appendChild(buttons);
    }

    return {

      render: function (exp) {

        var expname = exp[0];

        var text = '';
        if ((typeof exp[2][0]) == 'string') text = exp[2].shift();

        var atts = [];
        for (key in exp[1]) {
          var skey = key.replace(/-/, '_');
          var sval = exp[1][key];
          if ((typeof sval) == 'string') sval = '"' + sval + '"';
          atts.push("" + skey + ': ' + sval);
        }
        atts = atts.join(", ");

        var d = document.createElement('div');

        var sen = document.createElement('span');
        sen.setAttribute('class', 'tred_exp_span tred_expression_name');
        sen.appendChild(document.createTextNode(expname));
        d.appendChild(sen);

        var ses = document.createElement('span');
        ses.setAttribute('class', 'tred_exp_span tred_expression_string');
        var t = text;
        if (t != '') t = ' ' + t;
        ses.appendChild(document.createTextNode(t));
        d.appendChild(ses);

        var sea = document.createElement('span');
        sea.setAttribute('class', 'tred_exp_span tred_expression_atts');
        sea.appendChild(document.createTextNode(' ' + atts));
        d.appendChild(sea);

        addHeadButtons(d);

        var onblur = function () {

          var p = d.parentNode;
          var d2 = ExpressionHead.render(ExpressionHead.parse(this.value));
          p.replaceChild(d2, d);

          triggerChange(p); // trigger onChange()...
        };

        var onkeyup = function (evt) {

          var e = evt || window.event;
          var c = e.charCode || e.keyCode;
          if (c == 13) this.blur();

          return false;
        }

        var onclick = function () {
          d.removeChild(sen);
          d.removeChild(ses);
          var input = document.createElement('input');
          input.setAttribute('type', 'text');
          input.value = expname + ' ' + atts;
          if (text != '') input.value = expname + ' ' + text + ' ' + atts;
          d.replaceChild(input, sea);
          input.onblur = onblur;
          input.onkeyup = onkeyup;
          input.focus();
        };

        sen.onclick = onclick;
        ses.onclick = onclick;
        sea.onclick = onclick;

        return d;
      },

      parse: function (s) {

        var m = s.match(/^(\S*)( [.]*[^:]*)?( .*)?$/);

        if (m == null) return ['---', {}, []];

        var expname = m[1];

        var children = [];
        if (m[2]) {
          var t = m[2].tstrip();
          if (t != '') children.push(t);
        }

        var atts = m[3];
        atts = atts ? eval('({' + atts + '})') : {};

        return [ expname, atts, children ];
      },

      toExp: function (node) {
        node = node.firstChild;
        var name = node.childNodes[0].firstChild.nodeValue;
        var text = node.childNodes[1].firstChild.nodeValue;
        var atts = node.childNodes[2].firstChild.nodeValue;
        atts = eval('({' + atts + '})');
        var children = [];
        if (text != '') children.push(text);
        return [ name, atts, children ];
      }
    };
  }();

  function asJson (node) {

    if ((typeof node) == 'string')
      node = document.getElementById(node);

    return toJson(toTree(node));
  }

  //
  // turns a tree into a JSON string.
  //
  function toJson (tree) {

    if ((typeof tree) == 'string') return '"' + tree + '"';
    var attributes = tree[1];
    var children = tree[2];
    s = '["'+tree[0]+'",{';
    var atts = [];
    for (var attname in attributes) {
      atts.push('"' + attname + '":"' + attributes[attname] + '"');
    }
    s += atts.join(",");
    s += "},[";
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      s += toJson(child);
      if (i < children.length - 1) s += ",";
    }
    s += "]]"
    return s;
  }

  function onChange (tree) {

    alert("Tred.onChange(tree) : please override me");
  }

  function renderOpening (node, exp) {

    var opening = ExpressionHead.render(exp);

    //var outOpacity = 0.03;
    var outOpacity = 0.0;

    node.appendChild(opening);
  }

  function renderEnding (node, exp) {

    var ending = document.createElement("div");
    ending.className = "tred_text";
    if (exp[2].length > 0) ending.appendChild(document.createTextNode("end"));
    node.appendChild(ending);
  }

  function renderExpressionString (node, s) {

    var opening = document.createElement("div");

    var sname = document.createElement("span");
    sname.appendChild(document.createTextNode(s));
    sname.setAttribute("onclick", "EditableSpan.toInput(this);");
    sname.className = "tred_expression_string";
    opening.appendChild(sname);

    node.appendChild(opening);
  }

  function addExpression (parentExpNode, exp) {

    var end = parentExpNode.lastChild;
    var node = renderExpression(parentExpNode, exp);
    parentExpNode.replaceChild(node, end);
    parentExpNode.appendChild(end);

    triggerChange(parentExpNode);
  }

  function removeExpression (expNode) {

    var p = expNode.parentNode;
    p.removeChild(expNode);

    triggerChange(p);
  }

  function renderExpression (parentNode, exp, isRootExp) {

    //
    // draw expression

    var node = document.createElement("div");
    node.className = "tred_expression";
    if ( ! isRootExp) {
      //node.setAttribute("style", "margin-left: "+this.indentation+"px;");
      node.setAttribute("style", "margin-left: 14px;");
    }
    parentNode.appendChild(node);

    if ( ! (exp instanceof Array)) {

      renderExpressionString(node, exp.toString());
      return;
    }

    renderOpening(node, exp);

    //
    // draw children

    for (var i=0; i < exp[2].length; i++) {

      var child = exp[2][i];

      renderExpression(node, child);
    }

    renderEnding(node, exp);

    return node;
  }

  function renderFlow (parentNode, flow) {

    renderExpression(parentNode, flow, true);

    parentNode.className = "tred_root";

    parentNode.stack = []; // the undo stack
    parentNode.currentTree = flow;
  }

  function moveExpression (elt, delta) {

    var p = elt.parentNode;

    if (delta == -1) { // move up
      if (elt.previousSibling.className != "tred_expression") return;
      p.insertBefore(elt, elt.previousSibling);
    }
    else { // move down
      if (elt.nextSibling.className != "tred_expression") return;
      p.insertBefore(elt, elt.nextSibling.nextSibling);
    }

    Tred.triggerChange(p); // onChange() points to the original version !
  }

  function triggerChange (elt) {

    var tredRoot = findTredRoot(elt);
    var tree = toTree(tredRoot);

    stack(tredRoot, tree);

    Tred.onChange(tree); // onChange() points to the original version !
  }

  function stack(root, tree) {
    root.stack.push(root.currentTree);
    root.currentTree = tree;
    //while (root.stack.length > 35) root.stack.shift();
    //var s = "";
    //var st = toJson(tree);
    //s += ("current: " + st + " (" + st.length + ")\n\n");
    //for (var i = 0; i < root.stack.length; i++) {
    //  st = toJson(root.stack[i]);
    //  s += ("" + i + ": " + st + " (" + st.length + ")\n\n");
    //}
    //alert(s);
  }

  function undo (root) {

    if ((typeof root) == 'string') root = document.getElementById(root);
    if (root.stack.length < 1) return;

    while (root.firstChild != null) root.removeChild(root.firstChild);

    var tree = root.stack.pop();

    root.currentTree = tree;
    renderExpression(root, tree, true);

    //triggerChange(root);
    Tred.onChange(tree);
  }

  function findTredRoot (node) {

      if (node.className == 'tred_root') return node;
      return findTredRoot(node.parentNode);
  }

  function toTree (node) {

    node.focus();
      //
      // making sure all the input boxes got blurred...

    if (node.className != 'tred_expression') {
      node = node.firstChildOfClass('tred_expression');
    }

    //
    // expression itself

    /*
    var spans = node.getElementsByTagName("span");
    var expname = spans[0].firstChild.nodeValue;
    if (spans[0].className == 'tred_expression_string') return expname;
    var expatts = spans[1].firstChild.nodeValue;
    if (expatts == '---') expatts = '';
    */
    var exp = ExpressionHead.toExp(node);

    //
    // children

    var divs = node.childNodes;

    var children = exp[2];

    for (var i=0; i<divs.length; i++) {
      var e = divs[i];
      if (e.nodeType != 1) continue;
      if (e.className != "tred_expression") continue;
      children.push(toTree(e));
    }

    //
    // done

    //expatts = eval("({"+expatts+"})");
    return exp;
  }

  //
  // public methods
  //
  return {
    renderFlow: renderFlow,
    onChange: onChange,
    addExpression: addExpression,
    removeExpression: removeExpression,
    triggerChange: triggerChange,
    moveExpression: moveExpression,
    undo: undo,
    asJson: asJson
  };
}();

<commit_msg>implemented 'paste' (insertBefore)<commit_after>
/*
 *  OpenWFEru - open source ruby workflow and bpm engine
 *  (c) 2008 John Mettraux
 *
 *  OpenWFEru is freely distributable under the terms
 *  of a BSD-style license.
 *  For details, see the OpenWFEru web site: http://openwferu.rubyforge.org
 *
 *  This piece of hack was created during the RubyKaigi2008,
 *  between Tsukuba and Akihabara.
 */

//var flow = [ 'process-definition', { 'name': 'toto', 'revision': '1.0' }, [
//    [ 'sequence', {}, [
//      [ 'participant', { 'ref': 'alpha' }, [] ],
//      [ 'bravo', {}, [] ]
//    ]]
//  ]
//]

HTMLElement.prototype.firstChildOfClass = function (className) {

  for (var i=0; i < this.childNodes.length; i++) {
    var c = this.childNodes[i];
    if (c.className == className) return c;
  }

  return null;
}
String.prototype.tstrip = function () {
  var s = this;
  while (s.charAt(0) == ' ') s = s.substring(1);
  while (s.charAt(s.length - 1) == ' ') s = s.substring(0, s.length - 1);
  return s;
}

var Tred = function () {

  var ExpressionHead = function () {

    function createButton (imgsrc, tooltip, callback) {

      var i = document.createElement("img");
      i.callback = callback;
      i.className = "tred_button";
      i.setAttribute('src', imgsrc);
      i.setAttribute('title', tooltip);
      i.setAttribute("onclick", "this.callback()");
      return i;
    }

    function addHeadButtons (expdiv) {

      var outOpacity = 0.0;

      var buttons = document.createElement("span");
      buttons.style.opacity = outOpacity;

      expdiv.onmouseover = function () { buttons.style.opacity = 1.0; }
      expdiv.onmouseout = function () { buttons.style.opacity = outOpacity; }

      buttons.appendChild(createButton(
        'images/btn-add.gif',
        'add a child expression',
        function () {
          Tred.addExpression(expdiv.parentNode, [ "---", {}, [] ]);
        }));
      buttons.appendChild(createButton(
        'images/btn-cut.gif',
        'cut expression',
        function () {
          Tred.removeExpression(expdiv.parentNode);
        }));

      buttons.appendChild(createButton(
        'images/btn-moveup.gif',
        'move expression up',
        function () {
          Tred.moveExpression(expdiv.parentNode, -1);
          buttons.style.opacity = outOpacity;
        }));

      buttons.appendChild(createButton(
        'images/btn-movedown.gif',
        'move expression down',
        function () {
          Tred.moveExpression(expdiv.parentNode, +1);
          buttons.style.opacity = outOpacity;
        }));

      buttons.appendChild(createButton(
        'images/btn-paste.gif',
        'paste expression here',
        function () {
          var clip = document._tred_clipboard;
          if (clip) Tred.insertExpression(expdiv.parentNode, clip);
        }));

      expdiv.appendChild(buttons);
    }

    return {

      render: function (exp) {

        var expname = exp[0];

        var text = '';
        if ((typeof exp[2][0]) == 'string') text = exp[2].shift();

        var atts = [];
        for (key in exp[1]) {
          var skey = key.replace(/-/, '_');
          var sval = exp[1][key];
          if ((typeof sval) == 'string') sval = '"' + sval + '"';
          atts.push("" + skey + ': ' + sval);
        }
        atts = atts.join(", ");

        var d = document.createElement('div');

        var sen = document.createElement('span');
        sen.setAttribute('class', 'tred_exp_span tred_expression_name');
        sen.appendChild(document.createTextNode(expname));
        d.appendChild(sen);

        var ses = document.createElement('span');
        ses.setAttribute('class', 'tred_exp_span tred_expression_string');
        var t = text;
        if (t != '') t = ' ' + t;
        ses.appendChild(document.createTextNode(t));
        d.appendChild(ses);

        var sea = document.createElement('span');
        sea.setAttribute('class', 'tred_exp_span tred_expression_atts');
        sea.appendChild(document.createTextNode(' ' + atts));
        d.appendChild(sea);

        addHeadButtons(d);

        var onblur = function () {

          var p = d.parentNode;
          var d2 = ExpressionHead.render(ExpressionHead.parse(this.value));
          p.replaceChild(d2, d);

          triggerChange(p); // trigger onChange()...
        };

        var onkeyup = function (evt) {

          var e = evt || window.event;
          var c = e.charCode || e.keyCode;
          if (c == 13) this.blur();

          return false;
        }

        var onclick = function () {
          d.removeChild(sen);
          d.removeChild(ses);
          var input = document.createElement('input');
          input.setAttribute('type', 'text');
          input.value = expname + ' ' + atts;
          if (text != '') input.value = expname + ' ' + text + ' ' + atts;
          d.replaceChild(input, sea);
          input.onblur = onblur;
          input.onkeyup = onkeyup;
          input.focus();
        };

        sen.onclick = onclick;
        ses.onclick = onclick;
        sea.onclick = onclick;

        return d;
      },

      parse: function (s) {

        var m = s.match(/^(\S*)( [.]*[^:]*)?( .*)?$/);

        if (m == null) return ['---', {}, []];

        var expname = m[1];

        var children = [];
        if (m[2]) {
          var t = m[2].tstrip();
          if (t != '') children.push(t);
        }

        var atts = m[3];
        atts = atts ? eval('({' + atts + '})') : {};

        return [ expname, atts, children ];
      },

      toExp: function (node) {
        node = node.firstChild;
        var name = node.childNodes[0].firstChild.nodeValue;
        var text = node.childNodes[1].firstChild.nodeValue;
        var atts = node.childNodes[2].firstChild.nodeValue;
        atts = eval('({' + atts + '})');
        var children = [];
        if (text != '') children.push(text);
        return [ name, atts, children ];
      }
    };
  }();

  function asJson (node) {

    if ((typeof node) == 'string')
      node = document.getElementById(node);

    return toJson(toTree(node));
  }

  //
  // turns a tree into a JSON string.
  //
  function toJson (tree) {

    if ((typeof tree) == 'string') return '"' + tree + '"';
    var attributes = tree[1];
    var children = tree[2];
    s = '["'+tree[0]+'",{';
    var atts = [];
    for (var attname in attributes) {
      atts.push('"' + attname + '":"' + attributes[attname] + '"');
    }
    s += atts.join(",");
    s += "},[";
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      s += toJson(child);
      if (i < children.length - 1) s += ",";
    }
    s += "]]"
    return s;
  }

  function onChange (tree) {

    alert("Tred.onChange(tree) : please override me");
  }

  function renderOpening (node, exp) {

    var opening = ExpressionHead.render(exp);

    //var outOpacity = 0.03;
    var outOpacity = 0.0;

    node.appendChild(opening);
  }

  function renderEnding (node, exp) {

    var ending = document.createElement("div");
    ending.className = "tred_text";
    if (exp[2].length > 0) ending.appendChild(document.createTextNode("end"));
    node.appendChild(ending);
  }

  function renderExpressionString (node, s) {

    var opening = document.createElement("div");

    var sname = document.createElement("span");
    sname.appendChild(document.createTextNode(s));
    sname.setAttribute("onclick", "EditableSpan.toInput(this);");
    sname.className = "tred_expression_string";
    opening.appendChild(sname);

    node.appendChild(opening);
  }

  function addExpression (parentExpNode, exp) {

    var end = parentExpNode.lastChild;
    var node = renderExpression(parentExpNode, exp);
    parentExpNode.replaceChild(node, end);
    parentExpNode.appendChild(end);

    triggerChange(parentExpNode);
  }

  function removeExpression (expNode) {

    var p = expNode.parentNode;
    p.removeChild(expNode);

    document._tred_clipboard = toTree(expNode);

    triggerChange(p);
  }

  function renderExpression (parentNode, exp, isRootExp) {

    //
    // draw expression

    var node = document.createElement("div");
    node.className = "tred_expression";
    if ( ! isRootExp) {
      //node.setAttribute("style", "margin-left: "+this.indentation+"px;");
      node.setAttribute("style", "margin-left: 14px;");
    }
    parentNode.appendChild(node);

    if ( ! (exp instanceof Array)) {

      renderExpressionString(node, exp.toString());
      return;
    }

    renderOpening(node, exp);

    //
    // draw children

    for (var i=0; i < exp[2].length; i++) {

      var child = exp[2][i];

      renderExpression(node, child);
    }

    renderEnding(node, exp);

    return node;
  }

  function renderFlow (parentNode, flow) {

    renderExpression(parentNode, flow, true);

    parentNode.className = "tred_root";

    parentNode.stack = []; // the undo stack
    parentNode.currentTree = flow;
  }

  function moveExpression (elt, delta) {

    var p = elt.parentNode;

    if (delta == -1) { // move up
      if (elt.previousSibling.className != "tred_expression") return;
      p.insertBefore(elt, elt.previousSibling);
    }
    else { // move down
      if (elt.nextSibling.className != "tred_expression") return;
      p.insertBefore(elt, elt.nextSibling.nextSibling);
    }

    Tred.triggerChange(p); // onChange() points to the original version !
  }

  function insertExpression (before, exp) {

    var newNode = renderExpression(before.parentNode, exp);

    before.parentNode.insertBefore(newNode, before);

    Tred.triggerChange(before.parentNode);
  }

  function triggerChange (elt) {

    var tredRoot = findTredRoot(elt);
    var tree = toTree(tredRoot);

    stack(tredRoot, tree);

    Tred.onChange(tree); // onChange() points to the original version !
  }

  function stack(root, tree) {
    root.stack.push(root.currentTree);
    root.currentTree = tree;
  }

  function undo (root) {

    if ((typeof root) == 'string') root = document.getElementById(root);
    if (root.stack.length < 1) return;

    while (root.firstChild != null) root.removeChild(root.firstChild);

    var tree = root.stack.pop();

    root.currentTree = tree;
    renderExpression(root, tree, true);

    //triggerChange(root);
    Tred.onChange(tree);
  }

  function findTredRoot (node) {

      if (node.className == 'tred_root') return node;
      return findTredRoot(node.parentNode);
  }

  function toTree (node) {

    node.focus();
      //
      // making sure all the input boxes got blurred...

    if (node.className != 'tred_expression') {
      node = node.firstChildOfClass('tred_expression');
    }

    //
    // expression itself

    /*
    var spans = node.getElementsByTagName("span");
    var expname = spans[0].firstChild.nodeValue;
    if (spans[0].className == 'tred_expression_string') return expname;
    var expatts = spans[1].firstChild.nodeValue;
    if (expatts == '---') expatts = '';
    */
    var exp = ExpressionHead.toExp(node);

    //
    // children

    var divs = node.childNodes;

    var children = exp[2];

    for (var i=0; i<divs.length; i++) {
      var e = divs[i];
      if (e.nodeType != 1) continue;
      if (e.className != "tred_expression") continue;
      children.push(toTree(e));
    }

    //
    // done

    //expatts = eval("({"+expatts+"})");
    return exp;
  }

  //
  // public methods
  //
  return {
    renderFlow: renderFlow,
    onChange: onChange,
    addExpression: addExpression,
    removeExpression: removeExpression,
    moveExpression: moveExpression,
    insertExpression: insertExpression,
    triggerChange: triggerChange,
    undo: undo,
    asJson: asJson
  };
}();

<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var express = require('express');
var MongoStore = require('connect-mongo')(express);
var flash = require('express-flash');
var path = require('path');
var mongoose = require('mongoose');
var passport = require('passport');
var expressValidator = require('express-validator');
var connectAssets = require('connect-assets');
var _ = require('underscore');
var notifyService = require('./controllers/util/notificationService');
var OpenShiftApp = require('./openshift');

/**
 * Load controllers.
 */

var sitemap = require('./controllers/sitemap');
sitemap.scheduleSitemapRebuild(1000 * 60 * 60 * 24); // 1 day
var homeController = require('./controllers/home');
var userController = require('./controllers/user');
var apiController = require('./controllers/api');
//var contactController = require('./controllers/contact');
var createEventPage = require('./controllers/event/create-page');
var saveEvent = require('./controllers/event/save-event');
var viewEvent = require('./controllers/event/view');
var editEvent = require('./controllers/event/edit');
var cancelEvent = require('./controllers/event/cancel');
var upload = require('./controllers/upload').handleUpload;
var myEvents = require('./controllers/profile/my-events');
var rmEventImage = require('./controllers/event/rm-image');

var eventStore = require('./controllers/event/EventStore');

var renderTpl = require('./controllers/render-tpl').renderTpl;

/**
 * API keys + Passport configuration.
 */

var secrets = require('./config/secrets');
var passportConf = require('./config/passport');

var app = express();

app.set('ipaddress', '127.0.0.1'); // TODO refactor to use appConfig instead of plain strings
app.set('port', 3000);
app.set('mongodb-url', secrets.db);

var openShiftApp = new OpenShiftApp();
openShiftApp.initialize(app);

secrets.db = app.get('mongodb-url');

app.response.renderNotFound = function() {
  this.render('404', { status: 404 });
};

/**
 * Express configuration.
 */

var hour = 3600000;
var day = (hour * 24);
var month = (day * 30);

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(connectAssets({
  paths: ['public/css', 'public/js'],
  helperContext: app.locals,
  build: openShiftApp.isOpenShiftEnv() ? true : false
}));
app.use(express.compress());
app.use(express.logger('dev'));
app.use(express.cookieParser());
app.use(express.json());
app.use(express.urlencoded());
app.use(expressValidator());
app.use(express.methodOverride());
app.use(express.session({
  secret: secrets.sessionSecret,
  store: new MongoStore({
    url: secrets.db,
    auto_reconnect: true
  })
}));
app.use(express.csrf());
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
  res.locals.user = req.user;
  res.locals._csrf = req.csrfToken();
  res.locals.secrets = secrets;
  res.cookie('XSRF-TOKEN', req.csrfToken());
  next();
});
app.use(flash());
app.use(express.static(path.join(__dirname, 'public'), { maxAge: month }));
app.use(function(req, res, next) {
  // Keep track of previous URL
  if (req.method !== 'GET') return next();
  var path = req.path.split('/')[1];
  if (/(auth|login|logout|signup)$/i.test(path)) return next();
  var returnTo = req.path.indexOf('/assets') === 0 || req.path.indexOf('/favicon.ico') === 0 ? '/' : req.path; // ignore assets requests
  req.session.returnTo = returnTo;
  next();
});
app.use(app.router);
app.use(function(req, res) {
  res.renderNotFound();
});
app.use(express.errorHandler());

/**
 * Application routes.
 */

// nocache middleware, Use when you don't want browser to cache the request (e.g. on back button click)
function nocache(req, res, next) {
  res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate');
  res.set('Expires', '-1');
  res.set('Pragma', 'no-cache');
  next();
}

app.get('/', homeController.index);
app.get('/sitemap.xml', sitemap.getSitemap);
app.get('/login', userController.getLogin);
app.post('/login', userController.postLogin);
app.get('/logout', userController.logout);
app.get('/forgot', userController.getForgot);
app.post('/forgot', userController.postForgot);
app.get('/reset/:token', userController.getReset);
app.post('/reset/:token', userController.postReset);
app.get('/signup', userController.getSignup);
app.post('/signup', userController.postSignup);
//app.get('/contact', contactController.getContact);
//app.post('/contact', contactController.postContact);
app.get('/account', passportConf.isAuthenticated, userController.getAccount);
app.post('/account/profile', passportConf.isAuthenticated, userController.postUpdateProfile);
app.post('/account/password', passportConf.isAuthenticated, userController.postUpdatePassword);
app.post('/account/delete', passportConf.isAuthenticated, userController.postDeleteAccount);
app.get('/account/unlink/:provider', passportConf.isAuthenticated, userController.getOauthUnlink);
app.get('/api', apiController.getApi);
app.get('/api/lastfm', apiController.getLastfm);
app.get('/api/nyt', apiController.getNewYorkTimes);
app.get('/api/aviary', apiController.getAviary);
app.get('/api/paypal', apiController.getPayPal);
app.get('/api/paypal/success', apiController.getPayPalSuccess);
app.get('/api/paypal/cancel', apiController.getPayPalCancel);
app.get('/api/steam', apiController.getSteam);
app.get('/api/scraping', apiController.getScraping);
app.get('/api/twilio', apiController.getTwilio);
app.post('/api/twilio', apiController.postTwilio);
app.get('/api/clockwork', apiController.getClockwork);
app.post('/api/clockwork', apiController.postClockwork);
app.get('/api/foursquare', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFoursquare);
app.get('/api/tumblr', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTumblr);
app.get('/api/facebook', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFacebook);
app.get('/api/github', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getGithub);
app.get('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTwitter);
app.get('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getVenmo);
app.post('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postVenmo);
app.get('/api/linkedin', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getLinkedin);

var feedback = require('./controllers/feedback');
app.get('/tpl/*', renderTpl);
app.get('/feedback', feedback.get_feedback);
app.get('/event/create', passportConf.isAuthenticated, createEventPage);
app.post('/event/save', passportConf.isAuthenticated, saveEvent);
app.get('/event/:id', viewEvent);
app.get('/event/:id/edit', passportConf.isAuthenticated, editEvent);          // TODO move rest calls under /api/meetua
app.get('/event/:id/cancel', passportConf.isAuthenticated, cancelEvent);
app.post('/event/:id/rm-image/:imageId', passportConf.isAuthenticated, rmEventImage);
app.post('/upload/image', passportConf.isAuthenticated, upload);
app.get('/profile/my-events', passportConf.isAuthenticated, myEvents);

// MeetUA API
var meetuaEventsApi = require('./controllers/api/events');
app.post('/api/meetua/user/login', userController.postLoginRest);
app.get('/api/meetua/events/find', meetuaEventsApi.get_find); // TODO rename events -> event to keep routing consistency
app.get('/api/meetua/events/findById', meetuaEventsApi.get_findById);
app.get('/api/meetua/events/my', passportConf.isAuthenticatedRest, meetuaEventsApi.get_my);
app.get('/api/meetua/events/myOverview', nocache);
app.get('/api/meetua/events/myOverview', passportConf.isAuthenticatedRest, meetuaEventsApi.get_myOverview);
app.post('/api/meetua/events/participation', passportConf.isAuthenticatedRest, meetuaEventsApi.post_participation);

/**
 * OAuth routes for sign-in.
 */

app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'] }));
app.get('/auth/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/vkontakte', passport.authenticate('vkontakte', { scope: ['email'] }));
app.get('/auth/vkontakte/callback', passport.authenticate('vkontakte', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' }));
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get('/auth/twitter/callback', passport.authenticate('twitter', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/linkedin', passport.authenticate('linkedin', { state: 'SOME STATE' }));
app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});

/**
 * OAuth routes for API examples that require authorization.
 */

app.get('/auth/foursquare', passport.authorize('foursquare'));
app.get('/auth/foursquare/callback', passport.authorize('foursquare', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/foursquare');
});
app.get('/auth/tumblr', passport.authorize('tumblr'));
app.get('/auth/tumblr/callback', passport.authorize('tumblr', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/tumblr');
});
app.get('/auth/venmo', passport.authorize('venmo', { scope: 'make_payments access_profile access_balance access_email access_phone' }));
app.get('/auth/venmo/callback', passport.authorize('venmo', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/venmo');
});

/**
 * Start Express server.
 */

app.listen(app.get('port'), app.get('ipaddress'), function() {
  console.log("✔ Express server listening on %s:%d in %s mode", app.get('ipaddress'), app.get('port'), app.get('env'));
});

/**
 * Mongoose configuration.
 */

mongoose.connect(app.get('mongodb-url'));
mongoose.connection.on('error', function() {
  console.error('✗ MongoDB Connection Error. Please make sure MongoDB is running.');
});

if (!openShiftApp.isOpenShiftEnv()) {
  // DB preloading
  eventStore.dbPreload();
  userController.dbPreload();
}

notifyService.startCronJobs();

module.exports = app;
<commit_msg>Add HTTP header Content-Language: ru<commit_after>/**
 * Module dependencies.
 */

var express = require('express');
var MongoStore = require('connect-mongo')(express);
var flash = require('express-flash');
var path = require('path');
var mongoose = require('mongoose');
var passport = require('passport');
var expressValidator = require('express-validator');
var connectAssets = require('connect-assets');
var _ = require('underscore');
var notifyService = require('./controllers/util/notificationService');
var OpenShiftApp = require('./openshift');

/**
 * Load controllers.
 */

var sitemap = require('./controllers/sitemap');
sitemap.scheduleSitemapRebuild(1000 * 60 * 60 * 24); // 1 day
var homeController = require('./controllers/home');
var userController = require('./controllers/user');
var apiController = require('./controllers/api');
//var contactController = require('./controllers/contact');
var createEventPage = require('./controllers/event/create-page');
var saveEvent = require('./controllers/event/save-event');
var viewEvent = require('./controllers/event/view');
var editEvent = require('./controllers/event/edit');
var cancelEvent = require('./controllers/event/cancel');
var upload = require('./controllers/upload').handleUpload;
var myEvents = require('./controllers/profile/my-events');
var rmEventImage = require('./controllers/event/rm-image');

var eventStore = require('./controllers/event/EventStore');

var renderTpl = require('./controllers/render-tpl').renderTpl;

/**
 * API keys + Passport configuration.
 */

var secrets = require('./config/secrets');
var passportConf = require('./config/passport');

var app = express();

app.set('ipaddress', '127.0.0.1'); // TODO refactor to use appConfig instead of plain strings
app.set('port', 3000);
app.set('mongodb-url', secrets.db);

var openShiftApp = new OpenShiftApp();
openShiftApp.initialize(app);

secrets.db = app.get('mongodb-url');

app.response.renderNotFound = function() {
  this.render('404', { status: 404 });
};

/**
 * Express configuration.
 */

var hour = 3600000;
var day = (hour * 24);
var month = (day * 30);

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(connectAssets({
  paths: ['public/css', 'public/js'],
  helperContext: app.locals,
  build: openShiftApp.isOpenShiftEnv() ? true : false
}));
app.use(express.compress());
app.use(express.logger('dev'));
app.use(express.cookieParser());
app.use(express.json());
app.use(express.urlencoded());
app.use(expressValidator());
app.use(express.methodOverride());
app.use(express.session({
  secret: secrets.sessionSecret,
  store: new MongoStore({
    url: secrets.db,
    auto_reconnect: true
  })
}));
app.use(express.csrf());
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
  res.locals.user = req.user;
  res.locals._csrf = req.csrfToken();
  res.locals.secrets = secrets;
  res.cookie('XSRF-TOKEN', req.csrfToken());
  res.header('Content-Language', 'ru');
  next();
});
app.use(flash());
app.use(express.static(path.join(__dirname, 'public'), { maxAge: month }));
app.use(function(req, res, next) {
  // Keep track of previous URL
  if (req.method !== 'GET') return next();
  var path = req.path.split('/')[1];
  if (/(auth|login|logout|signup)$/i.test(path)) return next();
  var returnTo = req.path.indexOf('/assets') === 0 || req.path.indexOf('/favicon.ico') === 0 ? '/' : req.path; // ignore assets requests
  req.session.returnTo = returnTo;
  next();
});
app.use(app.router);
app.use(function(req, res) {
  res.renderNotFound();
});
app.use(express.errorHandler());

/**
 * Application routes.
 */

// nocache middleware, Use when you don't want browser to cache the request (e.g. on back button click)
function nocache(req, res, next) {
  res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate');
  res.set('Expires', '-1');
  res.set('Pragma', 'no-cache');
  next();
}

app.get('/', homeController.index);
app.get('/sitemap.xml', sitemap.getSitemap);
app.get('/login', userController.getLogin);
app.post('/login', userController.postLogin);
app.get('/logout', userController.logout);
app.get('/forgot', userController.getForgot);
app.post('/forgot', userController.postForgot);
app.get('/reset/:token', userController.getReset);
app.post('/reset/:token', userController.postReset);
app.get('/signup', userController.getSignup);
app.post('/signup', userController.postSignup);
//app.get('/contact', contactController.getContact);
//app.post('/contact', contactController.postContact);
app.get('/account', passportConf.isAuthenticated, userController.getAccount);
app.post('/account/profile', passportConf.isAuthenticated, userController.postUpdateProfile);
app.post('/account/password', passportConf.isAuthenticated, userController.postUpdatePassword);
app.post('/account/delete', passportConf.isAuthenticated, userController.postDeleteAccount);
app.get('/account/unlink/:provider', passportConf.isAuthenticated, userController.getOauthUnlink);
app.get('/api', apiController.getApi);
app.get('/api/lastfm', apiController.getLastfm);
app.get('/api/nyt', apiController.getNewYorkTimes);
app.get('/api/aviary', apiController.getAviary);
app.get('/api/paypal', apiController.getPayPal);
app.get('/api/paypal/success', apiController.getPayPalSuccess);
app.get('/api/paypal/cancel', apiController.getPayPalCancel);
app.get('/api/steam', apiController.getSteam);
app.get('/api/scraping', apiController.getScraping);
app.get('/api/twilio', apiController.getTwilio);
app.post('/api/twilio', apiController.postTwilio);
app.get('/api/clockwork', apiController.getClockwork);
app.post('/api/clockwork', apiController.postClockwork);
app.get('/api/foursquare', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFoursquare);
app.get('/api/tumblr', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTumblr);
app.get('/api/facebook', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFacebook);
app.get('/api/github', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getGithub);
app.get('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTwitter);
app.get('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getVenmo);
app.post('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postVenmo);
app.get('/api/linkedin', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getLinkedin);

var feedback = require('./controllers/feedback');
app.get('/tpl/*', renderTpl);
app.get('/feedback', feedback.get_feedback);
app.get('/event/create', passportConf.isAuthenticated, createEventPage);
app.post('/event/save', passportConf.isAuthenticated, saveEvent);
app.get('/event/:id', viewEvent);
app.get('/event/:id/edit', passportConf.isAuthenticated, editEvent);          // TODO move rest calls under /api/meetua
app.get('/event/:id/cancel', passportConf.isAuthenticated, cancelEvent);
app.post('/event/:id/rm-image/:imageId', passportConf.isAuthenticated, rmEventImage);
app.post('/upload/image', passportConf.isAuthenticated, upload);
app.get('/profile/my-events', passportConf.isAuthenticated, myEvents);

// MeetUA API
var meetuaEventsApi = require('./controllers/api/events');
app.post('/api/meetua/user/login', userController.postLoginRest);
app.get('/api/meetua/events/find', meetuaEventsApi.get_find); // TODO rename events -> event to keep routing consistency
app.get('/api/meetua/events/findById', meetuaEventsApi.get_findById);
app.get('/api/meetua/events/my', passportConf.isAuthenticatedRest, meetuaEventsApi.get_my);
app.get('/api/meetua/events/myOverview', nocache);
app.get('/api/meetua/events/myOverview', passportConf.isAuthenticatedRest, meetuaEventsApi.get_myOverview);
app.post('/api/meetua/events/participation', passportConf.isAuthenticatedRest, meetuaEventsApi.post_participation);

/**
 * OAuth routes for sign-in.
 */

app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'] }));
app.get('/auth/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/vkontakte', passport.authenticate('vkontakte', { scope: ['email'] }));
app.get('/auth/vkontakte/callback', passport.authenticate('vkontakte', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' }));
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get('/auth/twitter/callback', passport.authenticate('twitter', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});
app.get('/auth/linkedin', passport.authenticate('linkedin', { state: 'SOME STATE' }));
app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { failureRedirect: '/login' }), function(req, res) {
  res.redirect(req.session.returnTo || '/');
});

/**
 * OAuth routes for API examples that require authorization.
 */

app.get('/auth/foursquare', passport.authorize('foursquare'));
app.get('/auth/foursquare/callback', passport.authorize('foursquare', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/foursquare');
});
app.get('/auth/tumblr', passport.authorize('tumblr'));
app.get('/auth/tumblr/callback', passport.authorize('tumblr', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/tumblr');
});
app.get('/auth/venmo', passport.authorize('venmo', { scope: 'make_payments access_profile access_balance access_email access_phone' }));
app.get('/auth/venmo/callback', passport.authorize('venmo', { failureRedirect: '/api' }), function(req, res) {
  res.redirect('/api/venmo');
});

/**
 * Start Express server.
 */

app.listen(app.get('port'), app.get('ipaddress'), function() {
  console.log("✔ Express server listening on %s:%d in %s mode", app.get('ipaddress'), app.get('port'), app.get('env'));
});

/**
 * Mongoose configuration.
 */

mongoose.connect(app.get('mongodb-url'));
mongoose.connection.on('error', function() {
  console.error('✗ MongoDB Connection Error. Please make sure MongoDB is running.');
});

if (!openShiftApp.isOpenShiftEnv()) {
  // DB preloading
  eventStore.dbPreload();
  userController.dbPreload();
}

notifyService.startCronJobs();

module.exports = app;
<|endoftext|>


<commit_before>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.pattern',
  name: 'Singleton',

  methods: [
    function installInClass(cls) {
      var instance;
      var oldCreate = cls.create;
      cls.create = function() {
        return instance || ( instance = oldCreate.apply(this, arguments) );
      }
    }
  ]
});

// We only need one Singleton, so make it a Singleton.
foam.CLASS({
  refines: 'foam.pattern.Singleton',
  axioms: [ foam.pattern.Singleton.create() ]
});
<commit_msg>Made Singleton clone() and equals() faster.<commit_after>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.pattern',
  name: 'Singleton',

  methods: [
    function installInClass(cls) {
      var instance;
      var oldCreate = cls.create;
      cls.create = function() {
        return instance || ( instance = oldCreate.apply(this, arguments) );
      }
    },
    function clone() { return this; },
    function equals(other) { return other === this; }
  ]
});

// We only need one Singleton, so make it a Singleton.
foam.CLASS({
  refines: 'foam.pattern.Singleton',
  axioms: [ foam.pattern.Singleton.create() ]
});
<|endoftext|>


<commit_before>// Copyright 2017-2022, University of Colorado Boulder

/**
 * General help information for how to navigation a simulation with a keyboard. In general, this file supports a fair
 * number of options, like displaying group content, or shortcuts for checkbox interaction. The algorithm this type
 * implements set all the optional potential rows as null, and then fills them in if the options is provided. Then at the
 * end anything that is null is filtered out.
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import merge from '../../../../phet-core/js/merge.js';
import sceneryPhet from '../../sceneryPhet.js';
import sceneryPhetStrings from '../../sceneryPhetStrings.js';
import TextKeyNode from '../TextKeyNode.js';
import KeyboardHelpIconFactory from './KeyboardHelpIconFactory.js';
import KeyboardHelpSection from './KeyboardHelpSection.js';
import KeyboardHelpSectionRow from './KeyboardHelpSectionRow.js';

// constants
const keyboardHelpDialogExitADialogString = sceneryPhetStrings.keyboardHelpDialog.exitADialog;
const keyboardHelpDialogMoveBetweenItemsInAGroupString = sceneryPhetStrings.keyboardHelpDialog.moveBetweenItemsInAGroup;
const keyboardHelpDialogMoveToNextItemOrGroupString = sceneryPhetStrings.keyboardHelpDialog.moveToNextItemOrGroup;
const keyboardHelpDialogMoveToPreviousItemOrGroupString = sceneryPhetStrings.keyboardHelpDialog.moveToPreviousItemOrGroup;
const keyboardHelpDialogPressButtonsString = sceneryPhetStrings.keyboardHelpDialog.pressButtons;
const keyboardHelpDialogToggleCheckboxesString = sceneryPhetStrings.keyboardHelpDialog.toggleCheckboxes;
const keyboardHelpDialogTabGroupDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.tabGroupDescription;
const keyboardHelpDialogShiftTabGroupDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.shiftTabGroupDescription;
const keyboardHelpDialogPressButtonsDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.pressButtonsDescription;
const keyboardHelpDialogGroupNavigationDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.groupNavigationDescription;
const keyboardHelpDialogExitDialogDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.exitDialogDescription;
const toggleCheckboxesDescriptionString = sceneryPhetStrings.a11y.keyboardHelpDialog.general.toggleCheckboxesDescription;

class BasicActionsKeyboardHelpSection extends KeyboardHelpSection {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {

    options = merge( {
      withCheckboxContent: false // if true, the help content will include information about how to interact with checkboxes
    }, options );

    // 'press buttons' content
    const spaceIcon = TextKeyNode.space();
    const pressButtonsItemRow = KeyboardHelpSectionRow.labelWithIcon( keyboardHelpDialogPressButtonsString, spaceIcon, {
      labelInnerContent: keyboardHelpDialogPressButtonsDescriptionString
    } );

    // 'exit a dialog' content
    const exitADialogIcon = TextKeyNode.esc();
    const exitADialogRow = KeyboardHelpSectionRow.labelWithIcon( keyboardHelpDialogExitADialogString, exitADialogIcon, {
      labelInnerContent: keyboardHelpDialogExitDialogDescriptionString
    } );

    // 'toggle checkboxes' content
    let toggleCheckboxes = null;
    if ( options.withCheckboxContent ) {
      toggleCheckboxes = KeyboardHelpSectionRow.labelWithIcon( keyboardHelpDialogToggleCheckboxesString, TextKeyNode.space(),
        {
          labelInnerContent: toggleCheckboxesDescriptionString
        } );
    }

    const leftRightArrowsIcon = KeyboardHelpIconFactory.leftRightArrowKeysRowIcon();
    const upDownArrowsIcon = KeyboardHelpIconFactory.upDownArrowKeysRowIcon();
    const moveBetweenItemsInAGroupRow = KeyboardHelpSectionRow.labelWithIcon(
      keyboardHelpDialogMoveBetweenItemsInAGroupString,
      KeyboardHelpIconFactory.iconOrIcon( leftRightArrowsIcon, upDownArrowsIcon ),
      {
        labelInnerContent: keyboardHelpDialogGroupNavigationDescriptionString
      }
    );

    const moveToNextItemRow = KeyboardHelpSectionRow.labelWithIcon(
      keyboardHelpDialogMoveToNextItemOrGroupString,
      TextKeyNode.tab(),
      {
        labelInnerContent: keyboardHelpDialogTabGroupDescriptionString
      }
    );

    const moveToPreviousItemRow = KeyboardHelpSectionRow.labelWithIcon(
      keyboardHelpDialogMoveToPreviousItemOrGroupString,
      KeyboardHelpIconFactory.shiftPlusIcon( TextKeyNode.tab() ),
      {
        labelInnerContent: keyboardHelpDialogShiftTabGroupDescriptionString
      }
    );

    const content = [
      moveToNextItemRow,
      moveToPreviousItemRow,
      moveBetweenItemsInAGroupRow,
      pressButtonsItemRow,
      toggleCheckboxes,
      exitADialogRow
    ].filter( row => row !== null ); // If any optional rows are null, omit them.

    // order the rows of content
    super( sceneryPhetStrings.keyboardHelpDialog.basicActionsStringProperty, content, options );
  }
}

sceneryPhet.register( 'BasicActionsKeyboardHelpSection', BasicActionsKeyboardHelpSection );
export default BasicActionsKeyboardHelpSection;<commit_msg>some support for dynamic locale in keyboard help, https://github.com/phetsims/scenery-phet/issues/769<commit_after>// Copyright 2017-2022, University of Colorado Boulder

/**
 * General help information for how to navigation a simulation with a keyboard. In general, this file supports a fair
 * number of options, like displaying group content, or shortcuts for checkbox interaction. The algorithm this type
 * implements set all the optional potential rows as null, and then fills them in if the options is provided. Then at the
 * end anything that is null is filtered out.
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import merge from '../../../../phet-core/js/merge.js';
import sceneryPhet from '../../sceneryPhet.js';
import sceneryPhetStrings from '../../sceneryPhetStrings.js';
import TextKeyNode from '../TextKeyNode.js';
import KeyboardHelpIconFactory from './KeyboardHelpIconFactory.js';
import KeyboardHelpSection from './KeyboardHelpSection.js';
import KeyboardHelpSectionRow from './KeyboardHelpSectionRow.js';

class BasicActionsKeyboardHelpSection extends KeyboardHelpSection {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {

    options = merge( {
      withCheckboxContent: false // if true, the help content will include information about how to interact with checkboxes
    }, options );

    // 'press buttons' content
    const spaceIcon = TextKeyNode.space();
    const pressButtonsItemRow = KeyboardHelpSectionRow.labelWithIcon(
      sceneryPhetStrings.keyboardHelpDialog.pressButtonsStringProperty, spaceIcon, {
        labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.pressButtonsDescriptionStringProperty
      } );

    // 'exit a dialog' content
    const exitADialogIcon = TextKeyNode.esc();
    const exitADialogRow = KeyboardHelpSectionRow.labelWithIcon(
      sceneryPhetStrings.keyboardHelpDialog.exitADialogStringProperty, exitADialogIcon, {
        labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.exitDialogDescriptionStringProperty
      } );

    // 'toggle checkboxes' content
    let toggleCheckboxes = null;
    if ( options.withCheckboxContent ) {
      toggleCheckboxes = KeyboardHelpSectionRow.labelWithIcon(
        sceneryPhetStrings.keyboardHelpDialog.toggleCheckboxesStringProperty, TextKeyNode.space(), {
          labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.toggleCheckboxesDescriptionStringProperty
        } );
    }

    const leftRightArrowsIcon = KeyboardHelpIconFactory.leftRightArrowKeysRowIcon();
    const upDownArrowsIcon = KeyboardHelpIconFactory.upDownArrowKeysRowIcon();
    const moveBetweenItemsInAGroupRow = KeyboardHelpSectionRow.labelWithIcon(
      sceneryPhetStrings.keyboardHelpDialog.moveBetweenItemsInAGroupStringProperty,
      KeyboardHelpIconFactory.iconOrIcon( leftRightArrowsIcon, upDownArrowsIcon ), {
        labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.groupNavigationDescriptionStringProperty
      } );

    const moveToNextItemRow = KeyboardHelpSectionRow.labelWithIcon(
      sceneryPhetStrings.keyboardHelpDialog.moveToNextItemOrGroupStringProperty,
      TextKeyNode.tab(), {
        labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.tabGroupDescriptionStringProperty
      } );

    const moveToPreviousItemRow = KeyboardHelpSectionRow.labelWithIcon(
      sceneryPhetStrings.keyboardHelpDialog.moveToPreviousItemOrGroupStringProperty,
      KeyboardHelpIconFactory.shiftPlusIcon( TextKeyNode.tab() ), {
        labelInnerContent: sceneryPhetStrings.a11y.keyboardHelpDialog.general.shiftTabGroupDescriptionStringProperty
      } );

    const content = [
      moveToNextItemRow,
      moveToPreviousItemRow,
      moveBetweenItemsInAGroupRow,
      pressButtonsItemRow,
      toggleCheckboxes,
      exitADialogRow
    ].filter( row => row !== null ); // If any optional rows are null, omit them.

    // order the rows of content
    super( sceneryPhetStrings.keyboardHelpDialog.basicActionsStringProperty, content, options );
  }
}

sceneryPhet.register( 'BasicActionsKeyboardHelpSection', BasicActionsKeyboardHelpSection );
export default BasicActionsKeyboardHelpSection;<|endoftext|>


<commit_before>"use strict";

/*global mediaWiki, jQuery, OO, steep*/

/*
 A module provides some user-selectable things.

 ToDo:
 + set the size to keep it open
 + toggle behaviour
 + scrolling if too many versions
 + date formatting
 + click handling
 */
(function(mw, $, OO, steepVE){
    steepVE.VersionPicker = function(options) {
	if (!options) {
	    throw new Error("VersionPicker needs some options on construction.");
	}

	if (!options.collection) {
	    throw new Error("VersionPicker's options should have a 'collection' property to tell it which STEEP collection to look in.");
	};

	this.collection = options.collection;

	OO.ui.DropdownInputWidget.call(this, options);
    };

    OO.inheritClass(steepVE.VersionPicker, OO.ui.DropdownInputWidget);

    steepVE.VersionPicker.prototype._clear = function() {
	/*
	 Clear the currently selected value, and remove all the options from the list.

	 Does not affect the currentDocument property;
	 */
	this.setOptions();
    };

    steepVE.VersionPicker.prototype.setOptions = function(options) {
	var result = steepVE.VersionPicker.parent.prototype.setOptions.call(
	    this,
	    [{
		label: 'Latest',
		data: null
	    }].concat(options || [])
	);

	this.setDisabled(
	    !options || !options.length
	);
    };

    var changing = false;

    steepVE.VersionPicker.prototype.setValue = function(v) {
	var i = parseInt(v);

	if (i === this.getValue()) {
	    return;
	}

	if (changing) {
	    // Prevent event loops.
	    return;
	}

	try {
	    changing = true;

	    steepVE.VersionPicker.parent.prototype.setValue.call(
		this,
		isNaN(i) ? null : i
	    );
	} finally {
	    changing = false;
	}
    };

    steepVE.VersionPicker.prototype.clearVersions = function() {
	this.currentDocument = null;
	this._clear();
    };

    steepVE.VersionPicker.prototype.getValue = function() {
	var v = steepVE.VersionPicker.parent.prototype.getValue.call(this);
	if (v === 0 || v) {
	    return parseInt(v);
	} else {
	    /*
	     Return null instead of empty string, undefined etc.
	     */
	    return null;
	}
    };

    steepVE.VersionPicker.prototype.loadVersions = function(document, targetVersion) {
	var picker = this;

	if (document && this.currentDocument !== document) {
	    picker._clear();
	    picker.currentDocument = document;

	    jQuery.getJSON(
		['/channel', 'versions', picker.collection, document, 0].join('/'),
		{},
		function(data, result) {
		    if (result === 'success') {
			picker.setOptions(
			    data
				.reverse()
				.map(function(r) {
				    return {
					data: r.v,
					label: new Date(r.ts).toLocaleString()
				    };
				})
			);

			if (targetVersion !== undefined) {
			    picker.setValue(targetVersion);
			}
		    } else {
			picker._clear();
			throw new Error(result);
		    }
		}
	    );
	}
    };

}(mediaWiki, jQuery, OO, steep.ve));
<commit_msg>Improved comment on the version picker.<commit_after>"use strict";

/*global mediaWiki, jQuery, OO, steep*/

/*
 Provides a selection box which shows the different available versions of a process-model or map.
 */
(function(mw, $, OO, steepVE){
    steepVE.VersionPicker = function(options) {
	if (!options) {
	    throw new Error("VersionPicker needs some options on construction.");
	}

	if (!options.collection) {
	    throw new Error("VersionPicker's options should have a 'collection' property to tell it which STEEP collection to look in.");
	};

	this.collection = options.collection;

	OO.ui.DropdownInputWidget.call(this, options);
    };

    OO.inheritClass(steepVE.VersionPicker, OO.ui.DropdownInputWidget);

    steepVE.VersionPicker.prototype._clear = function() {
	/*
	 Clear the currently selected value, and remove all the options from the list.

	 Does not affect the currentDocument property;
	 */
	this.setOptions();
    };

    steepVE.VersionPicker.prototype.setOptions = function(options) {
	var result = steepVE.VersionPicker.parent.prototype.setOptions.call(
	    this,
	    [{
		label: 'Latest',
		data: null
	    }].concat(options || [])
	);

	this.setDisabled(
	    !options || !options.length
	);
    };

    var changing = false;

    steepVE.VersionPicker.prototype.setValue = function(v) {
	var i = parseInt(v);

	if (i === this.getValue()) {
	    return;
	}

	if (changing) {
	    // Prevent event loops.
	    return;
	}

	try {
	    changing = true;

	    steepVE.VersionPicker.parent.prototype.setValue.call(
		this,
		isNaN(i) ? null : i
	    );
	} finally {
	    changing = false;
	}
    };

    steepVE.VersionPicker.prototype.clearVersions = function() {
	this.currentDocument = null;
	this._clear();
    };

    steepVE.VersionPicker.prototype.getValue = function() {
	var v = steepVE.VersionPicker.parent.prototype.getValue.call(this);
	if (v === 0 || v) {
	    return parseInt(v);
	} else {
	    /*
	     Return null instead of empty string, undefined etc.
	     */
	    return null;
	}
    };

    steepVE.VersionPicker.prototype.loadVersions = function(document, targetVersion) {
	var picker = this;

	if (document && this.currentDocument !== document) {
	    picker._clear();
	    picker.currentDocument = document;

	    jQuery.getJSON(
		['/channel', 'versions', picker.collection, document, 0].join('/'),
		{},
		function(data, result) {
		    if (result === 'success') {
			picker.setOptions(
			    data
				.reverse()
				.map(function(r) {
				    return {
					data: r.v,
					label: new Date(r.ts).toLocaleString()
				    };
				})
			);

			if (targetVersion !== undefined) {
			    picker.setValue(targetVersion);
			}
		    } else {
			picker._clear();
			throw new Error(result);
		    }
		}
	    );
	}
    };

}(mediaWiki, jQuery, OO, steep.ve));
<|endoftext|>


<commit_before>// This JavaScript supporting implementing the per project form used
// for showing and editing information about a project.

var criterionCategoryValue = {};
var criteriaMetUrlRequired = {};
var criteriaNAJustificationRequired = {};
var criterionFuture = {};
var MIN_SHOULD_LENGTH = 5;

// Global - name of criterion we last selected as 'met'.
// Don't hide this criterion (yet), so that users can enter a justification.
var globalLastSelectedMet = '';
var globalHideMetnaCriteria = false;

// Do a polyfill for datalist if it's not already supported
// (e.g., Safari fails to support polyfill at the time of this writing).
// See https://github.com/thgreasi/datalist-polyfill/blob/master/README.md
function polyfillDatalist() {
  var nativedatalist = !!('list' in document.createElement('input')) &&
      !!(document.createElement('datalist') && window.HTMLDataListElement);
  if (!nativedatalist) {
    $('input[list]').each(function() {
      var availableTags = $('#' + $(this).attr('list')).find('option').map(
        function() {
          return this.value;
        }).get();
      $(this).autocomplete({ source: availableTags });
    });
  }
}

// Note: This regex needs to be logically the same as the one used in
// the server-side badge calculation, or it may confuse some users.
// See app/models/project.rb function "contains_url?".
function containsURL(justification) {
  if (!justification) {
    return false;
  } else {
    return !!justification.match(/https?:\/\/[^ ]{5}/);
  }
}

// Determine result for a given criterion, which is one of
// passing, barely, failing, or question.
// The result calculation here must match the equivalent routine
// implemented on the server to prevent confusion.
function criterionResult(criterion) {
  var criterionStatus = '#project_' + criterion + '_status';
  var justification = $('#project_' + criterion + '_justification').val();
  if (!justification) {
    justification = '';
  }
  if ($(criterionStatus + '_na').is(':checked')) {
    if (!criteriaNAJustificationRequired[criterion] ||
        justification.length >= MIN_SHOULD_LENGTH) {
      return 'passing';
    } else {
      return 'question';
    }
  } else if ($(criterionStatus + '_met').is(':checked')) {
    if (criteriaMetUrlRequired[criterion] && !containsURL(justification)) {
      // Odd case: met is claimed, but we're still missing information.
      return 'question';
    } else {
      return 'passing';
    }
  } else if (criterionCategoryValue[criterion] === 'SHOULD' &&
             $(criterionStatus + '_unmet').is(':checked') &&
             justification.length >= MIN_SHOULD_LENGTH) {
    return 'barely';
  } else if (criterionCategoryValue[criterion] === 'SUGGESTED' &&
            !($(criterionStatus + '_').is(':checked'))) {
    return 'barely';
  } else if ($(criterionStatus + '_').is(':checked')) {
    return 'question';
  } else {
    return 'failing';
  }
}

// This must match the criteria implemented in Ruby to prevent confusion.
function isEnough(criterion) {
  var result = criterionResult(criterion);
  return (result === 'passing' || result === 'barely');
}

function resetProgressBar() {
  var total = 0;
  var enough = 0;
  var percentage;
  var percentAsString;
  $.each(criterionCategoryValue, function(key, value) {
    if (!criterionFuture[key]) { // Only include non-future values
      total++;
      if (isEnough(key)) {
        enough++;
      }
    }
  });
  percentage = enough / total;
  percentAsString = Math.round(percentage * 100).toString() + '%';
  $('#badge-progress').attr('aria-valuenow', percentage).
                      text(percentAsString).css('width', percentAsString);
}

function resetCriterionResult(criterion) {
  var result = criterionResult(criterion);
  if (result === 'passing') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_check_img').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Enough for a badge!');
  } else if (result === 'barely') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_dash').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  } else if (result === 'question') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_question').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  } else {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_x_img').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  }
}

function changedJustificationText(criteria) {
  var criteriaJust = '#project_' + criteria + '_justification';
  var criteriaStatus = '#project_' + criteria + '_status';
  if ($(criteriaStatus + '_unmet').is(':checked') &&
       (criterionCategoryValue[criteria] === 'SHOULD') &&
       ($(criteriaJust).val().length < MIN_SHOULD_LENGTH)) {
    $(criteriaJust).addClass('required-data');
  } else if ($(criteriaStatus + '_met').is(':checked') &&
           criteriaMetUrlRequired[criteria] &&
           !containsURL($(criteriaJust).val())) {
    $(criteriaJust).addClass('required-data');
  } else if ($(criteriaStatus + '_na').is(':checked') &&
       (criteriaNAJustificationRequired[criteria]) &&
       ($(criteriaJust).val().length < MIN_SHOULD_LENGTH)) {
    $(criteriaJust).addClass('required-data');
  } else {
    $(criteriaJust).removeClass('required-data');
  }
  resetCriterionResult(criteria);
  resetProgressBar();
}

// Do we have any text in this field region?  Handle the variations.
function hasFieldTextInside(e) {
  var i;
  i = e.find('input[type="text"]');
  if (i && i.val()) {
    return true;
  }
  i = e.find('textarea');
  if (i && i.val()) {
    return true;
  }
  i = e.find('.discussion-markdown');
  if (i && i.text()) {
    return true;
  }
  return false;
}

// If we should, hide the criteria that are "Met" or N/A and are enough.
// Do NOT hide 'met' criteria that aren't enough (e.g., missing required URL),
// and do NOT hide the last-selected-met criterion (so users can enter/edit
// justification text).
function hideMetNA() {
  $.each(criterionCategoryValue, function(key, value) {
    if (globalHideMetnaCriteria && key !== globalLastSelectedMet &&
        ($('#project_' + key + '_status_met').is(':checked') ||
         $('#project_' + key + '_status_na').is(':checked')) &&
        isEnough(key)) {
      $('#' + key).addClass('hidden');
    } else {
      $('#' + key).removeClass('hidden');
    }
  });
  $('.hidable-text-entry').each(function() {
    if (globalHideMetnaCriteria && hasFieldTextInside($(this))) {
      $(this).addClass('hidden');
    } else {
      $(this).removeClass('hidden');
    }
  });
}

function updateCriteriaDisplay(criteria) {
  var criteriaJust = '#project_' + criteria + '_justification';
  var criteriaStatus = '#project_' + criteria + '_status';
  var justificationElement = document.getElementById('project_' +
                           criteria + '_justification');
  var justificationValue = '';
  if (justificationElement) {
    justificationValue = justificationElement.value;
  }
  if ($(criteriaStatus + '_met').is(':checked')) {
    var criteriaMetPlaceholder = criteria + '_met_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaMetPlaceholder).html().trim());
    if (document.getElementById(criteria + '_met_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_unmet').is(':checked')) {
    var criteriaUnmetPlaceholder = criteria + '_unmet_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaUnmetPlaceholder).html().trim());
    if (document.getElementById(criteria + '_unmet_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_na').is(':checked')) {
    var criteriaNaPlaceholder = criteria + '_na_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaNaPlaceholder).html().trim());
    if (document.getElementById(criteria + '_na_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_').is(':checked')) {
    $(criteriaJust).attr('placeholder', 'Please explain');
    $(criteriaJust).hide('fast');
  }
  // If there's old justification text, force showing it even if it
  // no longer makes sense (so they can fix it or change their mind).
  if (justificationValue.length > 0) {
    $(criteriaJust).show('fast');
  }
  if (globalHideMetnaCriteria) {
    // If we're hiding met criteria, walk through and hide them.
    // We don't need to keep running this if we are NOT hiding them,
    // which is the normal case.
    hideMetNA();
  }
  changedJustificationText(criteria);
}

function changeCriterion(criterion) {
  var criterionStatus = '#project_' + criterion + '_status';
  if ($(criterionStatus + '_met').is(':checked')) {
    globalLastSelectedMet = criterion;
  }
  updateCriteriaDisplay(criterion);
}

function ToggleHideMet(e) {
  globalHideMetnaCriteria = !globalHideMetnaCriteria;
  // Note that button text shows what WILL happen on click, so it
  // shows the REVERSED state (not the current state).
  if (globalHideMetnaCriteria) {
    $('#toggle-hide-metna-criteria')
      .addClass('active').html('Show met and N/A criteria');
  } else {
    $('#toggle-hide-metna-criteria')
      .removeClass('active').html('Hide met or N/A criteria');
  }
  hideMetNA();
}

function setupProjectField(criteria) {
  updateCriteriaDisplay(criteria);
  $('input[name="project[' + criteria + '_status]"]').click(
      function() {
        changeCriterion(criteria);
      });
  $('input[name="project[' + criteria + '_justification]"]').blur(
      function() {
        updateCriteriaDisplay(criteria);
      });
  $('#project_' + criteria + '_justification').on('input',
      function() {
        changedJustificationText(criteria);
      });
  $('#project_' + criteria + '_justification').on('keyup',
      function() {
        changedJustificationText(criteria);
      });
}

function ToggleDetailsDisplay(e) {
  var detailsTextID = e.target.id.
                        replace('_details_toggler', '_details_text');
  $('#' + detailsTextID).toggle('fast',
    function() {
      var buttonText;
      if ($('#' + detailsTextID).is(':hidden')) {
        buttonText = 'Show details';
      } else {
        buttonText = 'Hide details';
      }
      $('#' + e.target.id).html(buttonText);
    });
}

// Create mappings from criteria name to category and met_url_required.
// Eventually replace with just accessing classes directly via JavaScript.
function SetupCriteriaStructures() {
  $('.status-chooser').each(
    function(index) {
      var criterionName = $(this).find('.criterion-name').text();
      var res = $(this).find('.criterion-met-url-required').text();
      var val = 'true' === res;
      criteriaMetUrlRequired[criterionName] = val;
      criterionCategoryValue[criterionName] =
        $(this).find('.criterion-category').text();
      criterionFuture[criterionName] =
        $(this).find('.criterion-future').text() === 'true';
      criteriaNAJustificationRequired[criterionName] =
        $(this).find('.criterion-na-justification-required').text() === 'true';
    }
  );
}

// Setup display as soon as page is ready
$(document).ready(function() {
  // By default, hide details.  We do the hiding in JavaScript, so
  // those who disable JavaScript will still see the text
  // (they'll have no way to later reveal it).
  $('.details-text').hide('fast');
  $('.details-toggler').html('Show details');
  $('.details-toggler').click(ToggleDetailsDisplay);

  $('#show-all-details').click(function(e) {
    $('.details-text').show('fast');
    $('.details-toggler').html('Hide details');
  });
  $('#hide-all-details').click(function(e) {
    $('.details-text').hide('fast');
    $('.details-toggler').html('Show details');
  });

  // By default all panels are collapsed.  We do the collapsing in
  // in JavaScript, so users who disable JS will still see the sections.
  $('.remove-in').removeClass('in');
  // Only show open indicators if JS is enabled
  $('.close-by-default').attr('data-toggle', 'collapse').addClass('collapsed');
  $('.open-by-default').attr('data-toggle', 'collapse');

  // Open the correct panel when hash in url
  if (location.hash !== null && location.hash !== '' &&
      $(location.hash).length !== 0) {
    $('.collapse').removeClass('in');
    $('.open-by-default').addClass('collapsed');
    $(location.hash + '.collapse').collapse('show');
  }

  // Add location-hash on opening of a panel
  $('.collapse').on('show.bs.collapse', function(e) {
    location.hash = '#' + this.getAttribute('id');
  });

  // Force these values on page reload
  globalLastSelectedMet = '';
  globalHideMetnaCriteria = false;
  $('#toggle-hide-metna-criteria').click(function(e) {
    ToggleHideMet(e);
  });

  $('[data-toggle="tooltip"]').tooltip(); // Enable bootstrap tooltips

  // A form element with class onchange-submit automatically submits its
  // form whenever it is changed.
  $('.onchange-submit').change(function() {
    $(this).parents('form').submit();
  });

  if ($('#project_entry_form').length) {

    SetupCriteriaStructures();

    // Implement "press this button to make all crypto N/A"
    $('#all_crypto_na').click(function(e) {
      $.each(criterionCategoryValue, function(key, value) {
        if ((/^crypto/).test(key)) {
          $('#project_' + key + '_status_na').prop('checked', true);
        }
        updateCriteriaDisplay(key);
      });
      resetProgressBar();
    });

    // Use "imagesloaded" to wait for image load before displaying them
    imagesLoaded(document).on('always', function(instance) {
      // Set up the interactive displays of "enough".
      $.each(criterionCategoryValue, function(key, value) {
        setupProjectField(key);
      });
      resetProgressBar();
    });
  }

  // Polyfill datalist (for Safari users)
  polyfillDatalist();
});
<commit_msg>A few JS rearrangements<commit_after>// This JavaScript supporting implementing the per project form used
// for showing and editing information about a project.

var criterionCategoryValue = {};
var criteriaMetUrlRequired = {};
var criteriaNAJustificationRequired = {};
var criterionFuture = {};
var MIN_SHOULD_LENGTH = 5;

// Global - name of criterion we last selected as 'met'.
// Don't hide this criterion (yet), so that users can enter a justification.
var globalLastSelectedMet = '';
var globalHideMetnaCriteria = false;

// Do a polyfill for datalist if it's not already supported
// (e.g., Safari fails to support polyfill at the time of this writing).
// See https://github.com/thgreasi/datalist-polyfill/blob/master/README.md
function polyfillDatalist() {
  var nativedatalist = !!('list' in document.createElement('input')) &&
      !!(document.createElement('datalist') && window.HTMLDataListElement);
  if (!nativedatalist) {
    $('input[list]').each(function() {
      var availableTags = $('#' + $(this).attr('list')).find('option').map(
        function() {
          return this.value;
        }).get();
      $(this).autocomplete({ source: availableTags });
    });
  }
}

// Note: This regex needs to be logically the same as the one used in
// the server-side badge calculation, or it may confuse some users.
// See app/models/project.rb function "contains_url?".
function containsURL(justification) {
  if (!justification) {
    return false;
  } else {
    return !!justification.match(/https?:\/\/[^ ]{5}/);
  }
}

// Determine result for a given criterion, which is one of
// passing, barely, failing, or question.
// The result calculation here must match the equivalent routine
// implemented on the server to prevent confusion.
function criterionResult(criterion) {
  var criterionStatus = '#project_' + criterion + '_status';
  var justification = $('#project_' + criterion + '_justification').val();
  if (!justification) {
    justification = '';
  }
  if ($(criterionStatus + '_na').is(':checked')) {
    if (!criteriaNAJustificationRequired[criterion] ||
        justification.length >= MIN_SHOULD_LENGTH) {
      return 'passing';
    } else {
      return 'question';
    }
  } else if ($(criterionStatus + '_met').is(':checked')) {
    if (criteriaMetUrlRequired[criterion] && !containsURL(justification)) {
      // Odd case: met is claimed, but we're still missing information.
      return 'question';
    } else {
      return 'passing';
    }
  } else if (criterionCategoryValue[criterion] === 'SHOULD' &&
             $(criterionStatus + '_unmet').is(':checked') &&
             justification.length >= MIN_SHOULD_LENGTH) {
    return 'barely';
  } else if (criterionCategoryValue[criterion] === 'SUGGESTED' &&
            !($(criterionStatus + '_').is(':checked'))) {
    return 'barely';
  } else if ($(criterionStatus + '_').is(':checked')) {
    return 'question';
  } else {
    return 'failing';
  }
}

// This must match the criteria implemented in Ruby to prevent confusion.
function isEnough(criterion) {
  var result = criterionResult(criterion);
  return (result === 'passing' || result === 'barely');
}

function resetProgressBar() {
  var total = 0;
  var enough = 0;
  var percentage;
  var percentAsString;
  $.each(criterionCategoryValue, function(key, value) {
    if (!criterionFuture[key]) { // Only include non-future values
      total++;
      if (isEnough(key)) {
        enough++;
      }
    }
  });
  percentage = enough / total;
  percentAsString = Math.round(percentage * 100).toString() + '%';
  $('#badge-progress').attr('aria-valuenow', percentage).
                      text(percentAsString).css('width', percentAsString);
}

function resetCriterionResult(criterion) {
  var result = criterionResult(criterion);
  if (result === 'passing') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_check_img').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Enough for a badge!');
  } else if (result === 'barely') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_dash').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  } else if (result === 'question') {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_question').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  } else {
    $('#' + criterion + '_enough').
        attr('src', $('#result_symbol_x_img').attr('src')).
        attr('width', 40).attr('height', 40).
        attr('alt', 'Not enough for a badge.');
  }
}

function changedJustificationText(criteria) {
  var criteriaJust = '#project_' + criteria + '_justification';
  var criteriaStatus = '#project_' + criteria + '_status';
  if ($(criteriaStatus + '_unmet').is(':checked') &&
       (criterionCategoryValue[criteria] === 'SHOULD') &&
       ($(criteriaJust).val().length < MIN_SHOULD_LENGTH)) {
    $(criteriaJust).addClass('required-data');
  } else if ($(criteriaStatus + '_met').is(':checked') &&
           criteriaMetUrlRequired[criteria] &&
           !containsURL($(criteriaJust).val())) {
    $(criteriaJust).addClass('required-data');
  } else if ($(criteriaStatus + '_na').is(':checked') &&
       (criteriaNAJustificationRequired[criteria]) &&
       ($(criteriaJust).val().length < MIN_SHOULD_LENGTH)) {
    $(criteriaJust).addClass('required-data');
  } else {
    $(criteriaJust).removeClass('required-data');
  }
  resetCriterionResult(criteria);
  resetProgressBar();
}

// Do we have any text in this field region?  Handle the variations.
function hasFieldTextInside(e) {
  var i;
  i = e.find('input[type="text"]');
  if (i && i.val()) {
    return true;
  }
  i = e.find('textarea');
  if (i && i.val()) {
    return true;
  }
  i = e.find('.discussion-markdown');
  if (i && i.text()) {
    return true;
  }
  return false;
}

// If we should, hide the criteria that are "Met" or N/A and are enough.
// Do NOT hide 'met' criteria that aren't enough (e.g., missing required URL),
// and do NOT hide the last-selected-met criterion (so users can enter/edit
// justification text).
function hideMetNA() {
  $.each(criterionCategoryValue, function(key, value) {
    if (globalHideMetnaCriteria && key !== globalLastSelectedMet &&
        ($('#project_' + key + '_status_met').is(':checked') ||
         $('#project_' + key + '_status_na').is(':checked')) &&
        isEnough(key)) {
      $('#' + key).addClass('hidden');
    } else {
      $('#' + key).removeClass('hidden');
    }
  });
  $('.hidable-text-entry').each(function() {
    if (globalHideMetnaCriteria && hasFieldTextInside($(this))) {
      $(this).addClass('hidden');
    } else {
      $(this).removeClass('hidden');
    }
  });
}

function updateCriteriaDisplay(criteria) {
  var criteriaJust = '#project_' + criteria + '_justification';
  var criteriaStatus = '#project_' + criteria + '_status';
  var justificationElement = document.getElementById('project_' +
                           criteria + '_justification');
  var justificationValue = '';
  if (justificationElement) {
    justificationValue = justificationElement.value;
  }
  if ($(criteriaStatus + '_met').is(':checked')) {
    var criteriaMetPlaceholder = criteria + '_met_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaMetPlaceholder).html().trim());
    if (document.getElementById(criteria + '_met_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_unmet').is(':checked')) {
    var criteriaUnmetPlaceholder = criteria + '_unmet_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaUnmetPlaceholder).html().trim());
    if (document.getElementById(criteria + '_unmet_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_na').is(':checked')) {
    var criteriaNaPlaceholder = criteria + '_na_placeholder';
    $(criteriaJust).
       attr('placeholder',
         $('#' + criteriaNaPlaceholder).html().trim());
    if (document.getElementById(criteria + '_na_suppress')) {
      $(criteriaJust).hide('fast');
    } else {
      $(criteriaJust).show('fast');
    }
  } else if ($(criteriaStatus + '_').is(':checked')) {
    $(criteriaJust).attr('placeholder', 'Please explain');
    $(criteriaJust).hide('fast');
  }
  // If there's old justification text, force showing it even if it
  // no longer makes sense (so they can fix it or change their mind).
  if (justificationValue.length > 0) {
    $(criteriaJust).show('fast');
  }
  if (globalHideMetnaCriteria) {
    // If we're hiding met criteria, walk through and hide them.
    // We don't need to keep running this if we are NOT hiding them,
    // which is the normal case.
    hideMetNA();
  }
  changedJustificationText(criteria);
}

function changeCriterion(criterion) {
  var criterionStatus = '#project_' + criterion + '_status';
  if ($(criterionStatus + '_met').is(':checked')) {
    globalLastSelectedMet = criterion;
  }
  updateCriteriaDisplay(criterion);
}

function ToggleHideMet(e) {
  globalHideMetnaCriteria = !globalHideMetnaCriteria;
  // Note that button text shows what WILL happen on click, so it
  // shows the REVERSED state (not the current state).
  if (globalHideMetnaCriteria) {
    $('#toggle-hide-metna-criteria')
      .addClass('active').html('Show met and N/A criteria');
  } else {
    $('#toggle-hide-metna-criteria')
      .removeClass('active').html('Hide met or N/A criteria');
  }
  hideMetNA();
}

function setupProjectField(criteria) {
  updateCriteriaDisplay(criteria);
  $('input[name="project[' + criteria + '_status]"]').click(
      function() {
        changeCriterion(criteria);
      });
  $('input[name="project[' + criteria + '_justification]"]').blur(
      function() {
        updateCriteriaDisplay(criteria);
      });
  $('#project_' + criteria + '_justification').on('input',
      function() {
        changedJustificationText(criteria);
      });
  $('#project_' + criteria + '_justification').on('keyup',
      function() {
        changedJustificationText(criteria);
      });
}

function ToggleDetailsDisplay(e) {
  var detailsTextID = e.target.id.
                        replace('_details_toggler', '_details_text');
  $('#' + detailsTextID).toggle('fast',
    function() {
      var buttonText;
      if ($('#' + detailsTextID).is(':hidden')) {
        buttonText = 'Show details';
      } else {
        buttonText = 'Hide details';
      }
      $('#' + e.target.id).html(buttonText);
    });
}

// Create mappings from criteria name to category and met_url_required.
// Eventually replace with just accessing classes directly via JavaScript.
function SetupCriteriaStructures() {
  $('.status-chooser').each(
    function(index) {
      var criterionName = $(this).find('.criterion-name').text();
      var res = $(this).find('.criterion-met-url-required').text();
      var val = 'true' === res;
      criteriaMetUrlRequired[criterionName] = val;
      criterionCategoryValue[criterionName] =
        $(this).find('.criterion-category').text();
      criterionFuture[criterionName] =
        $(this).find('.criterion-future').text() === 'true';
      criteriaNAJustificationRequired[criterionName] =
        $(this).find('.criterion-na-justification-required').text() === 'true';
    }
  );
}

// Setup display as soon as page is ready
$(document).ready(function() {
  // By default, hide details.  We do the hiding in JavaScript, so
  // those who disable JavaScript will still see the text
  // (they'll have no way to later reveal it).
  $('.details-text').hide('fast');
  $('.details-toggler').html('Show details');
  $('.details-toggler').click(ToggleDetailsDisplay);

  $('#show-all-details').click(function(e) {
    $('.details-text').show('fast');
    $('.details-toggler').html('Hide details');
  });
  $('#hide-all-details').click(function(e) {
    $('.details-text').hide('fast');
    $('.details-toggler').html('Show details');
  });

  // Only show open indicators if JS is enabled
  $('.close-by-default').attr('data-toggle', 'collapse').addClass('collapsed');
  $('.open-by-default').attr('data-toggle', 'collapse');

  // Open the correct panel when hash in url
  if (location.hash !== null && location.hash !== '' &&
      $(location.hash).length !== 0) {
    $('.collapse').removeClass('in');
    $('.open-by-default').addClass('collapsed');
    $(location.hash + '.collapse').collapse('show');
  } else {
  // By default all panels are collapsed.  We do the collapsing in
  // in JavaScript, so users who disable JS will still see the sections.
    $('.remove-in').removeClass('in');
  }

  // Add location-hash on opening of a panel
  $('.collapse').on('show.bs.collapse', function(e) {
    location.hash = '#' + this.getAttribute('id');
  });

  // Force these values on page reload
  globalLastSelectedMet = '';
  globalHideMetnaCriteria = false;
  $('#toggle-hide-metna-criteria').click(function(e) {
    ToggleHideMet(e);
  });

  $('[data-toggle="tooltip"]').tooltip(); // Enable bootstrap tooltips

  // A form element with class onchange-submit automatically submits its
  // form whenever it is changed.
  $('.onchange-submit').change(function() {
    $(this).parents('form').submit();
  });

  if ($('#project_entry_form').length) {

    SetupCriteriaStructures();

    // Implement "press this button to make all crypto N/A"
    $('#all_crypto_na').click(function(e) {
      $.each(criterionCategoryValue, function(key, value) {
        if ((/^crypto/).test(key)) {
          $('#project_' + key + '_status_na').prop('checked', true);
        }
        updateCriteriaDisplay(key);
      });
      resetProgressBar();
    });

    // Use "imagesloaded" to wait for image load before displaying them
    imagesLoaded(document).on('always', function(instance) {
      // Set up the interactive displays of "enough".
      $.each(criterionCategoryValue, function(key, value) {
        setupProjectField(key);
      });
      resetProgressBar();
    });
  }

  // Polyfill datalist (for Safari users)
  polyfillDatalist();
});
<|endoftext|>