<commit_before>$(document).on('ready', function () {
  var hasDuplicates = function (array) {
    // debugger
    var sortedValues = array.sort();
    for(var i = 1; i < sortedValues.length; i++) {
      if(sortedValues[i] && (sortedValues[i] === sortedValues[i-1])) {
        return true;
      }
    }
    return false;
  }

  var generateMatrix = function () {
    var rows = $('tr');
    var matrix = _.map(rows, function (row, rowIndex) {
      var cells = $(row).find('td');
      return _.map(cells, function (cell, cellIndex) {
        var value = $(cell).find('input').val();
        var set = value ? true : false;
        return {
          rowIndex: rowIndex,
          cellIndex: cellIndex,
          set: set,
          value: value
        }
      })
    });
    return matrix;
  }

  var hasRowConflictAt = function (rowIndex, matrix) {
    var row = matrix[rowIndex];
    var values = _.map(row, function (cell) {
      return (cell.value === "") ? undefined : +cell.value;
    });
    return hasDuplicates(values);
  }

  var hasRowConflicts = function (matrix) {
    for(var i = 0; i < matrix.length; i++) {
      if(hasRowConflictAt(i, matrix)) {
        return true;
      }
    }
    return false;
  }

  var hasColConflictAt = function (colIndex, matrix) {
    var colValues = _.map(matrix, function (row) {
      return +row[colIndex].value;
    });
    return hasDuplicates(colValues);
  }

  var hasColConflicts = function (matrix) {
    for(var i = 0; i < matrix.length; i++) {
      if(hasColConflictAt(i, matrix)) {
        return true;
      }
    }
    return false;
  }

  // rowGroup describes a grouping of rows as 1, 2, or 3, to describe the first, second, or third three rows, respectively
  // colGroup describes a grouping of cols as 1, 2, or 3, to describe the first, second, or third three cols, respectively
  // a group is therefore described in terms of a rowGroup and a colGroup, such as (2,2) for the center group

  var hasGroupConflictAt = function (rowGroup, colGroup, matrix) {
    var groups = {
      1: [0,3],
      2: [3,6],
      3: [6,9]
    }
    var rowGroupCoordinates = groups[rowGroup];
    var colGroupCoordinates = groups[colGroup];
    var group = [].slice.apply(matrix, rowGroupCoordinates).map(function (row) {
      return [].slice.apply(row, colGroupCoordinates);
    });
    var groupValues = _.flatten(group).map(function (cell) {
      return +cell.value;
    });
    var sortedValues = groupValues.sort();
    return hasDuplicates(sortedValues);
  }

  var hasGroupConflicts = function (matrix) {
    for(var rowGroupIndex = 1; rowGroupIndex < 4; rowGroupIndex++) {
      for(var colGroupIndex = 1; colGroupIndex < 4; colGroupIndex++) {
        if(hasGroupConflictAt(rowGroupIndex, colGroupIndex, matrix)) {
          return true;
        }
      }
    }
    return false;
  }

  $('.solve')
    .on('click', function (e) {
      e.preventDefault();

      var matrix = generateMatrix();
      var hasConflicts = function () {
        return hasRowConflicts(matrix) || hasColConflicts(matrix) || hasGroupConflicts(matrix);
      }
      var setValue = function(rowIndex, colIndex, value) {
        matrix[rowIndex][colIndex].value = value;
      }
      var removeValue = function(rowIndex, colIndex) {
        matrix[rowIndex][colIndex].value = 0;
      }
      var mapValues = function () {
        return matrix.map(function (row) {
          return row.map(function (cell) {
            return cell.value;
          });
        });
      }
      console.log('mapValues right before declaration', mapValues)

      var inner = function (rowIndex, colIndex, value) {
        var cell = matrix[rowIndex][colIndex];
        console.log('INNER:','rowIndex', rowIndex, 'colIndex', colIndex, 'value', value);
        if(cell.set) { // if the cell has been pre-set...
          return inner(rowIndex, ++colIndex, 1); // move to the next column
        } else if (!cell.set) { // otherwise...
          setValue(rowIndex, colIndex, value); // set the specified input
          var conflict = hasConflicts(); // check for conflicts
          if(conflict) { // if there is a conflict ...
            removeValue(rowIndex, colIndex) // remove the value...
            if(value < 9) { // if the attempted input is less than 9...
              return inner(rowIndex, colIndex, ++value);
            } else { // otherwise
              debugger
              return false; // conflict is unavoidable using numbers 1-9 - go back down the line and change the values that came before
            }
          } else if (!conflict) { // if there is no conflict with the newly set value
            var nextColumn = colIndex < 8; // see if there is a next column
            var nextRow = rowIndex < 8; // see if there is a new row
            if(nextColumn) { // if there is a new column....
              var nextColWorks = inner(rowIndex, ++colIndex, 1); // move on to the next column and check to see if it provides a solution
              if(nextColWorks) { // if it provides a solution
                return nextColWorks; // return the solution
              } else { // otherwise
                console.log('nextCol didnt work:','rowIndex', rowIndex, 'colIndex', colIndex, 'value', value);
                debugger
                var nextValWorks = inner(rowIndex, colIndex, ++value); // stay in this column and increment the value
                if(nextValWorks) {
                  return nextValWorks;
                } else {
                  console.log('nextVal didnt work:','rowIndex', rowIndex, 'colIndex', colIndex, 'value', value);
                  debugger
                  return false;
                }
              }
            } else if (nextRow) { // if there is no next column, see if there is a next row
              console.log('nextRow', rowIndex+1);
              return inner(++rowIndex, 0, 1); // if so, move on to the next row
            } else if (!nextColumn && !nextRow) { // if we are on the last cell of the last row
              return 'no next column or row'
              // return matrix; // we have found a solution matrix, so we'll return it
            }
          }
        }
      }
      console.log('mapValues right after declaration', mapValues)

      var result = inner(0,0,1);
      console.log(result);
    }); // end on click

  $('.clear')
    .on('click', function (e) {
      e.preventDefault();
      $('input').val('');
    });

  $('.tr1').find('.td1 input').focus();

}); // end on ready

<commit_msg>added parameter for reverse direction for cases where the column is decremented<commit_after>$(document).on('ready', function () {
  var hasDuplicates = function (array) {
    // debugger
    var sortedValues = array.sort();
    for(var i = 1; i < sortedValues.length; i++) {
      if(sortedValues[i] && (sortedValues[i] === sortedValues[i-1])) {
        return true;
      }
    }
    return false;
  }

  var generateMatrix = function () {
    var rows = $('tr');
    var matrix = _.map(rows, function (row, rowIndex) {
      var cells = $(row).find('td');
      return _.map(cells, function (cell, cellIndex) {
        var value = $(cell).find('input').val();
        var set = value ? true : false;
        return {
          rowIndex: rowIndex,
          cellIndex: cellIndex,
          set: set,
          value: value
        }
      })
    });
    return matrix;
  }

  var hasRowConflictAt = function (rowIndex, matrix) {
    var row = matrix[rowIndex];
    var values = _.map(row, function (cell) {
      return (cell.value === "") ? undefined : +cell.value;
    });
    return hasDuplicates(values);
  }

  var hasRowConflicts = function (matrix) {
    for(var i = 0; i < matrix.length; i++) {
      if(hasRowConflictAt(i, matrix)) {
        return true;
      }
    }
    return false;
  }

  var hasColConflictAt = function (colIndex, matrix) {
    var colValues = _.map(matrix, function (row) {
      return +row[colIndex].value;
    });
    return hasDuplicates(colValues);
  }

  var hasColConflicts = function (matrix) {
    for(var i = 0; i < matrix.length; i++) {
      if(hasColConflictAt(i, matrix)) {
        return true;
      }
    }
    return false;
  }

  // rowGroup describes a grouping of rows as 1, 2, or 3, to describe the first, second, or third three rows, respectively
  // colGroup describes a grouping of cols as 1, 2, or 3, to describe the first, second, or third three cols, respectively
  // a group is therefore described in terms of a rowGroup and a colGroup, such as (2,2) for the center group

  var hasGroupConflictAt = function (rowGroup, colGroup, matrix) {
    var groups = {
      1: [0,3],
      2: [3,6],
      3: [6,9]
    }
    var rowGroupCoordinates = groups[rowGroup];
    var colGroupCoordinates = groups[colGroup];
    var group = [].slice.apply(matrix, rowGroupCoordinates).map(function (row) {
      return [].slice.apply(row, colGroupCoordinates);
    });
    var groupValues = _.flatten(group).map(function (cell) {
      return +cell.value;
    });
    var sortedValues = groupValues.sort();
    return hasDuplicates(sortedValues);
  }

  var hasGroupConflicts = function (matrix) {
    for(var rowGroupIndex = 1; rowGroupIndex < 4; rowGroupIndex++) {
      for(var colGroupIndex = 1; colGroupIndex < 4; colGroupIndex++) {
        if(hasGroupConflictAt(rowGroupIndex, colGroupIndex, matrix)) {
          return true;
        }
      }
    }
    return false;
  }

  $('.solve')
    .on('click', function (e) {
      e.preventDefault();

      var matrix = generateMatrix();
      var hasConflicts = function () {
        return hasRowConflicts(matrix) || hasColConflicts(matrix) || hasGroupConflicts(matrix);
      }
      var setValue = function(rowIndex, colIndex, value) {
        matrix[rowIndex][colIndex].value = value;
      }
      var removeValue = function(rowIndex, colIndex) {
        matrix[rowIndex][colIndex].value = 0;
      }
      var mapValues = function () {
        return matrix.map(function (row) {
          return row.map(function (cell) {
            return cell.value;
          });
        });
      }
      console.log('mapValues right before declaration', mapValues)

      var inner = function (rowIndex, colIndex, value, reverse) {
        console.log('mapValues', mapValues)
        console.log('INNER:', '(',rowIndex,',', colIndex,')', 'value', value);

        var cell = matrix[rowIndex][colIndex];
        if(cell.set) { // if the cell has been pre-set...
          if(reverse) {
            return inner(rowIndex, --colIndex, value);
          }
          return inner(rowIndex, ++colIndex, 1); // move to the next column
        } else if (!cell.set) { // otherwise...
          setValue(rowIndex, colIndex, value); // set the specified input
          var conflict = hasConflicts(); // check for conflicts
          if(conflict) { // if there is a conflict ...
            console.log('conflict, removing value:', '(',rowIndex,',', colIndex,')', 'value', value);
            removeValue(rowIndex, colIndex) // remove the value...
            // debugger
            if(value < 9) { // if the attempted input is less than 9...
              return inner(rowIndex, colIndex, ++value); // try with the next value up
            } else { // otherwise
              // debugger
              return false; // conflict is unavoidable using numbers 1-9 - go back down the line and change the values that came before
            }

          } else if (!conflict) { // if there is no conflict with the newly set value
            var nextColumn = colIndex < 8; // see if there is a next column
            var nextRow = rowIndex < 8; // see if there is a new row
            if(nextColumn) { // if there is a next column....
              var nextColWorks = inner(rowIndex, ++colIndex, 1); // move on to the next column and check to see if it provides a solution
              if(nextColWorks) { // if it provides a solution
                return nextColWorks; // return the solution
              } else { // otherwise
                console.log('nextCol didnt work:', '(',rowIndex,',', colIndex,')', 'value', value);
                debugger
                if(value < 9) { // if the value is less than 9...
                  var nextValWorks = inner(rowIndex, --colIndex, ++value, true); // remain in this column, increment the value, and check for a solution
                  if(nextValWorks) { // if there is a solution...
                    return nextValWorks; // return it
                  } else { // otherwise
                    return false;  // end recursion
                  }
                } else { // if the value is not less than 9...
                  return false; // end recursion
                }
              }
            } else if (nextRow) { // if there is no next column, see if there is a next row
              console.log('nextRow', rowIndex+1);
              return inner(++rowIndex, 0, 1); // if so, move on to the next row
            } else if (!nextColumn && !nextRow) { // if we are on the last cell of the last row
              debugger
              return 'no next column or row'
              // return matrix; // we have found a solution matrix, so we'll return it
            }
          }
        }
      }
      console.log('mapValues right after declaration', mapValues)

      var result = inner(0,0,1);
      console.log(result);
    }); // end on click

  $('.clear')
    .on('click', function (e) {
      e.preventDefault();
      $('input').val('');
    });

  $('.tr1').find('.td1 input').focus();

}); // end on ready

<|endoftext|>


<commit_before>/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */

'use strict';

const IMEManager = {
  BASIC_LAYOUT: -1,
  ALTERNATE_LAYOUT: -2,
  SWITCH_KEYBOARD: -3,
  TOGGLE_CANDIDATE_PANEL: -4,

  // IME Engines are self registering here.
  IMEngines: {},
  get currentEngine() {
    return this.IMEngines[Keyboards[this.currentKeyboard].imEngine];
  },

  // TODO: allow user to select desired keyboards in settings
  // see bug 712778
  currentKeyboard: 'qwertyLayout',
  currentKeyboardMode: '',
  keyboards: [
    'qwertyLayout', 'azertyLayout', 'qwertzLayout', 'hebrewLayout',
    'jcukenLayout', 'serbianCyrillicLayout', 'dvorakLayout',
    'zhuyingGeneralLayout'
  ],

  get isUpperCase() {
    return (this.currentKeyboardMode == 'UpperCase');
  },

  set isUpperCase(isUpperCase) {
    var keyboard = this.currentKeyboard;
    if (isUpperCase) {
      keyboard += 'UpperCase';
      this.currentKeyboardMode = 'UpperCase';
    } else {
      this.currentKeyboardMode = '';
    }
    this.updateLayout(keyboard);
  },

  get isAlternateLayout() {
    var alternateLayouts = ['Alternate', 'Symbol'];
    return alternateLayouts.indexOf(this.currentKeyboardMode) > -1;
  },

  set isAlternateLayout(isAlternateLayout) {
    if (isAlternateLayout) {
      this.currentKeyboardMode = 'Alternate';
      this.updateLayout('alternateLayout');
    } else {
      this.currentKeyboardMode = '';
      this.updateLayout(this.currentKeyboard);
    }
  },

  get isSymbolLayout() {
    return this.currentKeyboardMode == 'Symbol';
  },

  set isSymbolLayout(isSymbolLayout) {
    if (isSymbolLayout) {
      this.currentKeyboardMode = 'Symbol';
      this.updateLayout('symbolLayout');
    } else {
      this.currentKeyboardMode = '';
      this.updateLayout('alternateLayout');
    }
  },

  // backspace repeat delay and repeat rate
  kRepeatTimeout: 700,
  kRepeatRate: 100,

  // Taps the shift key twice within kCapsLockTimeout
  // to lock the keyboard at upper case state.
  kCapsLockTimeout: 450,
  isUpperCaseLocked: false,

  get ime() {
    delete this.ime;
    return this.ime = document.getElementById('keyboard');
  },

  get candidatePanel() {
    delete this.candidatePanel;
    var candidatePanel = document.createElement('div');
    candidatePanel.id = 'keyboard-candidate-panel';
    return this.candidatePanel = candidatePanel;
  },

  get keyHighlight() {
    return document.getElementById('keyboard-key-highlight');
  },

  events: ['showime', 'hideime', 'unload', 'appclose'],
  imeEvents: ['touchstart', 'touchend', 'click'],
  init: function km_init() {
    this.events.forEach((function attachEvents(type) {
      window.addEventListener(type, this);
    }).bind(this));

    this.imeEvents.forEach((function imeEvents(type) {
      this.ime.addEventListener(type, this);
    }).bind(this));

    this.keyboards.forEach((function loadIMEngines(name) {
      var keyboard = Keyboards[name];
      if (keyboard.type !== 'ime')
        return;

      var sourceDir = './imes/';
      var imEngine = keyboard.imEngine;

      var script = document.createElement('script');
      script.src = sourceDir + imEngine + '/loader.js';

      var self = this;
      var glue = {
        dbOptions: {
          data: sourceDir + imEngine + '/data.json'
        },
        sendChoices: function(candidates) {
          self.showCandidates(candidates);
        },
        sendKey: function(keyCode) {
          switch (keyCode) {
            case KeyEvent.DOM_VK_BACK_SPACE:
            case KeyEvent.DOM_VK_RETURN:
              window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
            break;

            default:
              // XXX until bug 713498 lands in git m-c, always send a keyCode
              // that is not 0
              //window.navigator.mozKeyboard.sendKey(0, keyCode);
              window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
            break;
          }
        },
        sendString: function(str) {
          for (var i = 0; i < str.length; i++)
            this.sendKey(str.charCodeAt(i));
        }
      };

      script.addEventListener('load', (function IMEnginesLoaded() {
        var engine = this.IMEngines[imEngine];
        engine.init(glue);
      }).bind(this));

      document.body.appendChild(script);
    }).bind(this));
  },

  uninit: function km_uninit() {
    this.events.forEach((function attachEvents(type) {
      window.removeEventListener(type, this);
    }).bind(this));

    this.imeEvents.forEach((function imeEvents(type) {
      this.ime.removeEventListener(type, this);
    }).bind(this));
  },

  handleEvent: function km_handleEvent(evt) {
    var activeWindow = Gaia.AppManager.foregroundWindow;
    var target = evt.target;
    switch (evt.type) {
      case 'showime':
        this.showIME(activeWindow, evt.detail.type);
        break;

      case 'hideime':
      case 'appclose':
        this.hideIME(activeWindow);
        break;

      case 'touchstart':
        var keyCode = parseInt(target.getAttribute('data-keycode'));
        target.dataset.active = 'true';

        if (!keyCode && !target.dataset.selection)
          return;

        this.keyHighlight.innerHTML = target.innerHTML;
        this.keyHighlight.className = 'show';
        this.keyHighlight.style.top = target.offsetTop.toString(10) + 'px';

        var keyHightlightWidth = this.keyHighlight.offsetWidth;
        var keyHightlightLeft =
          target.offsetLeft + target.offsetWidth / 2 - keyHightlightWidth / 2;
        keyHightlightLeft = Math.max(keyHightlightLeft, 5);
        keyHightlightLeft =
          Math.min(
            keyHightlightLeft,
            window.innerWidth - keyHightlightWidth - 5
          );

        this.keyHighlight.style.left = keyHightlightLeft.toString(10) + 'px';

        if (keyCode != KeyEvent.DOM_VK_BACK_SPACE)
          return;

        var sendDelete = (function sendDelete() {
          if (Keyboards[this.currentKeyboard].type == 'ime') {
            this.currentEngine.click(keyCode);
            return;
          }
          window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
        }).bind(this);

        sendDelete();
        this._timeout = setTimeout((function deleteTimeout() {
          sendDelete();

          this._interval = setInterval(function deleteInterval() {
            sendDelete();
          }, this.kRepeatRate);
        }).bind(this), this.kRepeatTimeout);
        break;

      case 'touchend':
        var keyCode = parseInt(target.getAttribute('data-keycode'));
        delete target.dataset.active;

        if (!keyCode && !target.dataset.selection)
          return;

        this.keyHighlight.className = '';

        clearTimeout(this._timeout);
        clearInterval(this._interval);
        break;

      case 'click':
        if (target.dataset.selection) {
          this.currentEngine.select(target.textContent, target.dataset.data);
          return;
        }

        var keyCode = parseInt(target.getAttribute('data-keycode'));
        if (!keyCode)
          return;

        if (keyCode == KeyEvent.DOM_VK_BACK_SPACE)
          return;

        switch (keyCode) {
          case this.BASIC_LAYOUT:
            this.isAlternateLayout = false;
          break;

          case this.ALTERNATE_LAYOUT:
            this.isAlternateLayout = true;
          break;

          case this.SWITCH_KEYBOARD:
            // If this is the last keyboard in the stack, start
            // back from the beginning.
            var keyboards = this.keyboards;
            var index = keyboards.indexOf(this.currentKeyboard);
            if (index >= keyboards.length - 1)
              this.currentKeyboard = keyboards[0];
            else
              this.currentKeyboard = keyboards[++index];

            this.updateLayout(this.currentKeyboard);
          break;

          case this.TOGGLE_CANDIDATE_PANEL:
            var panel = this.candidatePanel;
            var className = (panel.className == 'full') ? 'show' : 'full';
            panel.className = target.className = className;
          break;

          case KeyEvent.DOM_VK_ALT:
            this.isSymbolLayout = !this.isSymbolLayout;
          break;

          case KeyEvent.DOM_VK_CAPS_LOCK:
            if (this.isWaitingForSecondTap) {
              this.isUpperCaseLocked = true;
              if (!this.isUpperCase) {
                this.isUpperCase = true;

                // XXX: keyboard updated; target is lost.
                var selector =
                  'span[data-keycode="' + KeyEvent.DOM_VK_CAPS_LOCK + '"]';
                target = document.querySelector(selector);
              }
              target.dataset.enabled = 'true';
              delete this.isWaitingForSecondTap;
              break;
            }
            this.isWaitingForSecondTap = true;

            setTimeout(
              (function removeCapsLockTimeout() {
                delete this.isWaitingForSecondTap;
              }).bind(this),
              this.kCapsLockTimeout
            );

            this.isUpperCaseLocked = false;
            this.isUpperCase = !this.isUpperCase;
          break;

          case KeyEvent.DOM_VK_RETURN:
            if (Keyboards[this.currentKeyboard].type == 'ime') {
              this.currentEngine.click(keyCode);
              break;
            }

            window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
          break;

          default:
            if (Keyboards[this.currentKeyboard].type == 'ime') {
              this.currentEngine.click(keyCode);
              break;
            }

            // XXX until bug 713498 lands in git m-c, always send a keyCode
            // that is not 0
            //window.navigator.mozKeyboard.sendKey(0, keyCode);
            window.navigator.mozKeyboard.sendKey(keyCode, keyCode);

            if (this.isUpperCase && !this.isUpperCaseLocked)
              this.isUpperCase = false;
          break;
        }
        break;

      case 'unload':
        this.uninit();
        break;
    }
  },

  updateLayout: function km_updateLayout(keyboard) {
    var layout = Keyboards[keyboard];

    var content = '';
    var width = window.innerWidth;
    layout.keys.forEach(function buildKeyboardRow(row) {
      content += '
';

      row.forEach(function buildKeyboardColumns(key) {
        var code = key.keyCode || key.value.charCodeAt(0);
        var size = ((width - (row.length * 2)) / (layout.width || 10));
        size = size * (key.ratio || 1) - 2;
        var className = 'keyboard-key';

        var specialCodes = [
          KeyEvent.DOM_VK_BACK_SPACE,
          KeyEvent.DOM_VK_CAPS_LOCK,
          KeyEvent.DOM_VK_RETURN,
          KeyEvent.DOM_VK_ALT
        ];
        if (code < 0 || specialCodes.indexOf(code) > -1)
          className += ' keyboard-key-special';

        if (code == KeyEvent.DOM_VK_CAPS_LOCK)
          className += ' toggle';

        content += '' +
                   key.value +
                   '';
      });
      content += '
';
    });

    content += '';

    this.ime.innerHTML = content;

    if (layout.needsCandidatePanel) {
      var toggleButton = document.createElement('span');
      toggleButton.innerHTML = '⇪';
      toggleButton.id = 'keyboard-candidate-panel-toggle-button';
      toggleButton.dataset.keycode = this.TOGGLE_CANDIDATE_PANEL;
      this.ime.insertBefore(toggleButton, this.ime.firstChild);

      this.ime.insertBefore(this.candidatePanel, this.ime.firstChild);
      this.showCandidates([]);
      this.currentEngine.empty();
    }

    this.updateKeyboardHeight();
  },

  updateKeyboardHeight: function km_updateKeyboardHeight() {
    var ime = this.ime;
    var targetWindow = this.targetWindow;

    if (ime.offsetHeight !== 0) {
      targetWindow.classList.add('noTransition');
      setTimeout(function remoteNoTransition() {
        targetWindow.classList.remove('noTransition');
      }, 0);
    }

    // Need these to correctly measure scrollHeight
    ime.style.height = null;
    ime.style.overflowY = 'hidden';
    var scrollHeight = ime.scrollHeight;
    ime.style.overflowY = null;

    targetWindow.style.height =
      (targetWindow.dataset.rectHeight - scrollHeight) + 'px';
    ime.style.height = scrollHeight + 'px';
  },

  showIME: function km_showIME(targetWindow, type) {
    this.targetWindow = targetWindow;
    var oldHeight = targetWindow.style.height;
    targetWindow.dataset.cssHeight = oldHeight;
    targetWindow.dataset.rectHeight =
      targetWindow.getBoundingClientRect().height;

    this.updateLayout(this.currentKeyboard);
    delete this.ime.dataset.hidden;
  },

  hideIME: function km_hideIME(targetWindow) {
    var ime = this.ime;
    var imeHide = (function(evt) {
      targetWindow.removeEventListener('transitionend', imeHide);
      delete this.targetWindow;

      ime.innerHTML = '';
    }).bind(this);

    targetWindow.addEventListener('transitionend', imeHide);
    targetWindow.style.height = targetWindow.dataset.cssHeight;
    delete targetWindow.dataset.cssHeight;
    delete targetWindow.dataset.rectHeight;

    ime.style.height = null;
    ime.dataset.hidden = 'true';
  },

  showCandidates: function km_showCandidates(candidates) {
    // TODO: candidate panel should be allow toggled to fullscreen
    var candidatePanel = document.getElementById('keyboard-candidate-panel');
    var toggleButton =
      document.getElementById('keyboard-candidate-panel-toggle-button');

    candidatePanel.innerHTML = '';

    if (!candidates.length) {
      toggleButton.className = '';
      candidatePanel.className = '';
      this.updateKeyboardHeight();
      return;
    }

    toggleButton.className = toggleButton.className || 'show';
    candidatePanel.className = candidatePanel.className || 'show';

    if (toggleButton.className == 'show')
      this.updateKeyboardHeight();

    candidates.forEach(function buildCandidateEntry(candidate) {
      var span = document.createElement('span');
      span.dataset.data = candidate[1];
      span.dataset.selection = true;
      span.textContent = candidate[0];
      candidatePanel.appendChild(span);
    });
  }
};

window.addEventListener('load', function initIMEManager(evt) {
  window.removeEventListener('load', initIMEManager);
  IMEManager.init();
});

<commit_msg>Revert "Keep sending non-zero keyCode until bug 713498 lands" since it has landed<commit_after>/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */

'use strict';

const IMEManager = {
  BASIC_LAYOUT: -1,
  ALTERNATE_LAYOUT: -2,
  SWITCH_KEYBOARD: -3,
  TOGGLE_CANDIDATE_PANEL: -4,

  // IME Engines are self registering here.
  IMEngines: {},
  get currentEngine() {
    return this.IMEngines[Keyboards[this.currentKeyboard].imEngine];
  },

  // TODO: allow user to select desired keyboards in settings
  // see bug 712778
  currentKeyboard: 'qwertyLayout',
  currentKeyboardMode: '',
  keyboards: [
    'qwertyLayout', 'azertyLayout', 'qwertzLayout', 'hebrewLayout',
    'jcukenLayout', 'serbianCyrillicLayout', 'dvorakLayout',
    'zhuyingGeneralLayout'
  ],

  get isUpperCase() {
    return (this.currentKeyboardMode == 'UpperCase');
  },

  set isUpperCase(isUpperCase) {
    var keyboard = this.currentKeyboard;
    if (isUpperCase) {
      keyboard += 'UpperCase';
      this.currentKeyboardMode = 'UpperCase';
    } else {
      this.currentKeyboardMode = '';
    }
    this.updateLayout(keyboard);
  },

  get isAlternateLayout() {
    var alternateLayouts = ['Alternate', 'Symbol'];
    return alternateLayouts.indexOf(this.currentKeyboardMode) > -1;
  },

  set isAlternateLayout(isAlternateLayout) {
    if (isAlternateLayout) {
      this.currentKeyboardMode = 'Alternate';
      this.updateLayout('alternateLayout');
    } else {
      this.currentKeyboardMode = '';
      this.updateLayout(this.currentKeyboard);
    }
  },

  get isSymbolLayout() {
    return this.currentKeyboardMode == 'Symbol';
  },

  set isSymbolLayout(isSymbolLayout) {
    if (isSymbolLayout) {
      this.currentKeyboardMode = 'Symbol';
      this.updateLayout('symbolLayout');
    } else {
      this.currentKeyboardMode = '';
      this.updateLayout('alternateLayout');
    }
  },

  // backspace repeat delay and repeat rate
  kRepeatTimeout: 700,
  kRepeatRate: 100,

  // Taps the shift key twice within kCapsLockTimeout
  // to lock the keyboard at upper case state.
  kCapsLockTimeout: 450,
  isUpperCaseLocked: false,

  get ime() {
    delete this.ime;
    return this.ime = document.getElementById('keyboard');
  },

  get candidatePanel() {
    delete this.candidatePanel;
    var candidatePanel = document.createElement('div');
    candidatePanel.id = 'keyboard-candidate-panel';
    return this.candidatePanel = candidatePanel;
  },

  get keyHighlight() {
    return document.getElementById('keyboard-key-highlight');
  },

  events: ['showime', 'hideime', 'unload', 'appclose'],
  imeEvents: ['touchstart', 'touchend', 'click'],
  init: function km_init() {
    this.events.forEach((function attachEvents(type) {
      window.addEventListener(type, this);
    }).bind(this));

    this.imeEvents.forEach((function imeEvents(type) {
      this.ime.addEventListener(type, this);
    }).bind(this));

    this.keyboards.forEach((function loadIMEngines(name) {
      var keyboard = Keyboards[name];
      if (keyboard.type !== 'ime')
        return;

      var sourceDir = './imes/';
      var imEngine = keyboard.imEngine;

      var script = document.createElement('script');
      script.src = sourceDir + imEngine + '/loader.js';

      var self = this;
      var glue = {
        dbOptions: {
          data: sourceDir + imEngine + '/data.json'
        },
        sendChoices: function(candidates) {
          self.showCandidates(candidates);
        },
        sendKey: function(keyCode) {
          switch (keyCode) {
            case KeyEvent.DOM_VK_BACK_SPACE:
            case KeyEvent.DOM_VK_RETURN:
              window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
            break;

            default:
              window.navigator.mozKeyboard.sendKey(0, keyCode);
            break;
          }
        },
        sendString: function(str) {
          for (var i = 0; i < str.length; i++)
            this.sendKey(str.charCodeAt(i));
        }
      };

      script.addEventListener('load', (function IMEnginesLoaded() {
        var engine = this.IMEngines[imEngine];
        engine.init(glue);
      }).bind(this));

      document.body.appendChild(script);
    }).bind(this));
  },

  uninit: function km_uninit() {
    this.events.forEach((function attachEvents(type) {
      window.removeEventListener(type, this);
    }).bind(this));

    this.imeEvents.forEach((function imeEvents(type) {
      this.ime.removeEventListener(type, this);
    }).bind(this));
  },

  handleEvent: function km_handleEvent(evt) {
    var activeWindow = Gaia.AppManager.foregroundWindow;
    var target = evt.target;
    switch (evt.type) {
      case 'showime':
        this.showIME(activeWindow, evt.detail.type);
        break;

      case 'hideime':
      case 'appclose':
        this.hideIME(activeWindow);
        break;

      case 'touchstart':
        var keyCode = parseInt(target.getAttribute('data-keycode'));
        target.dataset.active = 'true';

        if (!keyCode && !target.dataset.selection)
          return;

        this.keyHighlight.innerHTML = target.innerHTML;
        this.keyHighlight.className = 'show';
        this.keyHighlight.style.top = target.offsetTop.toString(10) + 'px';

        var keyHightlightWidth = this.keyHighlight.offsetWidth;
        var keyHightlightLeft =
          target.offsetLeft + target.offsetWidth / 2 - keyHightlightWidth / 2;
        keyHightlightLeft = Math.max(keyHightlightLeft, 5);
        keyHightlightLeft =
          Math.min(
            keyHightlightLeft,
            window.innerWidth - keyHightlightWidth - 5
          );

        this.keyHighlight.style.left = keyHightlightLeft.toString(10) + 'px';

        if (keyCode != KeyEvent.DOM_VK_BACK_SPACE)
          return;

        var sendDelete = (function sendDelete() {
          if (Keyboards[this.currentKeyboard].type == 'ime') {
            this.currentEngine.click(keyCode);
            return;
          }
          window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
        }).bind(this);

        sendDelete();
        this._timeout = setTimeout((function deleteTimeout() {
          sendDelete();

          this._interval = setInterval(function deleteInterval() {
            sendDelete();
          }, this.kRepeatRate);
        }).bind(this), this.kRepeatTimeout);
        break;

      case 'touchend':
        var keyCode = parseInt(target.getAttribute('data-keycode'));
        delete target.dataset.active;

        if (!keyCode && !target.dataset.selection)
          return;

        this.keyHighlight.className = '';

        clearTimeout(this._timeout);
        clearInterval(this._interval);
        break;

      case 'click':
        if (target.dataset.selection) {
          this.currentEngine.select(target.textContent, target.dataset.data);
          return;
        }

        var keyCode = parseInt(target.getAttribute('data-keycode'));
        if (!keyCode)
          return;

        if (keyCode == KeyEvent.DOM_VK_BACK_SPACE)
          return;

        switch (keyCode) {
          case this.BASIC_LAYOUT:
            this.isAlternateLayout = false;
          break;

          case this.ALTERNATE_LAYOUT:
            this.isAlternateLayout = true;
          break;

          case this.SWITCH_KEYBOARD:
            // If this is the last keyboard in the stack, start
            // back from the beginning.
            var keyboards = this.keyboards;
            var index = keyboards.indexOf(this.currentKeyboard);
            if (index >= keyboards.length - 1)
              this.currentKeyboard = keyboards[0];
            else
              this.currentKeyboard = keyboards[++index];

            this.updateLayout(this.currentKeyboard);
          break;

          case this.TOGGLE_CANDIDATE_PANEL:
            var panel = this.candidatePanel;
            var className = (panel.className == 'full') ? 'show' : 'full';
            panel.className = target.className = className;
          break;

          case KeyEvent.DOM_VK_ALT:
            this.isSymbolLayout = !this.isSymbolLayout;
          break;

          case KeyEvent.DOM_VK_CAPS_LOCK:
            if (this.isWaitingForSecondTap) {
              this.isUpperCaseLocked = true;
              if (!this.isUpperCase) {
                this.isUpperCase = true;

                // XXX: keyboard updated; target is lost.
                var selector =
                  'span[data-keycode="' + KeyEvent.DOM_VK_CAPS_LOCK + '"]';
                target = document.querySelector(selector);
              }
              target.dataset.enabled = 'true';
              delete this.isWaitingForSecondTap;
              break;
            }
            this.isWaitingForSecondTap = true;

            setTimeout(
              (function removeCapsLockTimeout() {
                delete this.isWaitingForSecondTap;
              }).bind(this),
              this.kCapsLockTimeout
            );

            this.isUpperCaseLocked = false;
            this.isUpperCase = !this.isUpperCase;
          break;

          case KeyEvent.DOM_VK_RETURN:
            if (Keyboards[this.currentKeyboard].type == 'ime') {
              this.currentEngine.click(keyCode);
              break;
            }

            window.navigator.mozKeyboard.sendKey(keyCode, keyCode);
          break;

          default:
            if (Keyboards[this.currentKeyboard].type == 'ime') {
              this.currentEngine.click(keyCode);
              break;
            }

            window.navigator.mozKeyboard.sendKey(0, keyCode);

            if (this.isUpperCase && !this.isUpperCaseLocked)
              this.isUpperCase = false;
          break;
        }
        break;

      case 'unload':
        this.uninit();
        break;
    }
  },

  updateLayout: function km_updateLayout(keyboard) {
    var layout = Keyboards[keyboard];

    var content = '';
    var width = window.innerWidth;
    layout.keys.forEach(function buildKeyboardRow(row) {
      content += '
';

      row.forEach(function buildKeyboardColumns(key) {
        var code = key.keyCode || key.value.charCodeAt(0);
        var size = ((width - (row.length * 2)) / (layout.width || 10));
        size = size * (key.ratio || 1) - 2;
        var className = 'keyboard-key';

        var specialCodes = [
          KeyEvent.DOM_VK_BACK_SPACE,
          KeyEvent.DOM_VK_CAPS_LOCK,
          KeyEvent.DOM_VK_RETURN,
          KeyEvent.DOM_VK_ALT
        ];
        if (code < 0 || specialCodes.indexOf(code) > -1)
          className += ' keyboard-key-special';

        if (code == KeyEvent.DOM_VK_CAPS_LOCK)
          className += ' toggle';

        content += '' +
                   key.value +
                   '';
      });
      content += '
';
    });

    content += '';

    this.ime.innerHTML = content;

    if (layout.needsCandidatePanel) {
      var toggleButton = document.createElement('span');
      toggleButton.innerHTML = '⇪';
      toggleButton.id = 'keyboard-candidate-panel-toggle-button';
      toggleButton.dataset.keycode = this.TOGGLE_CANDIDATE_PANEL;
      this.ime.insertBefore(toggleButton, this.ime.firstChild);

      this.ime.insertBefore(this.candidatePanel, this.ime.firstChild);
      this.showCandidates([]);
      this.currentEngine.empty();
    }

    this.updateKeyboardHeight();
  },

  updateKeyboardHeight: function km_updateKeyboardHeight() {
    var ime = this.ime;
    var targetWindow = this.targetWindow;

    if (ime.offsetHeight !== 0) {
      targetWindow.classList.add('noTransition');
      setTimeout(function remoteNoTransition() {
        targetWindow.classList.remove('noTransition');
      }, 0);
    }

    // Need these to correctly measure scrollHeight
    ime.style.height = null;
    ime.style.overflowY = 'hidden';
    var scrollHeight = ime.scrollHeight;
    ime.style.overflowY = null;

    targetWindow.style.height =
      (targetWindow.dataset.rectHeight - scrollHeight) + 'px';
    ime.style.height = scrollHeight + 'px';
  },

  showIME: function km_showIME(targetWindow, type) {
    this.targetWindow = targetWindow;
    var oldHeight = targetWindow.style.height;
    targetWindow.dataset.cssHeight = oldHeight;
    targetWindow.dataset.rectHeight =
      targetWindow.getBoundingClientRect().height;

    this.updateLayout(this.currentKeyboard);
    delete this.ime.dataset.hidden;
  },

  hideIME: function km_hideIME(targetWindow) {
    var ime = this.ime;
    var imeHide = (function(evt) {
      targetWindow.removeEventListener('transitionend', imeHide);
      delete this.targetWindow;

      ime.innerHTML = '';
    }).bind(this);

    targetWindow.addEventListener('transitionend', imeHide);
    targetWindow.style.height = targetWindow.dataset.cssHeight;
    delete targetWindow.dataset.cssHeight;
    delete targetWindow.dataset.rectHeight;

    ime.style.height = null;
    ime.dataset.hidden = 'true';
  },

  showCandidates: function km_showCandidates(candidates) {
    // TODO: candidate panel should be allow toggled to fullscreen
    var candidatePanel = document.getElementById('keyboard-candidate-panel');
    var toggleButton =
      document.getElementById('keyboard-candidate-panel-toggle-button');

    candidatePanel.innerHTML = '';

    if (!candidates.length) {
      toggleButton.className = '';
      candidatePanel.className = '';
      this.updateKeyboardHeight();
      return;
    }

    toggleButton.className = toggleButton.className || 'show';
    candidatePanel.className = candidatePanel.className || 'show';

    if (toggleButton.className == 'show')
      this.updateKeyboardHeight();

    candidates.forEach(function buildCandidateEntry(candidate) {
      var span = document.createElement('span');
      span.dataset.data = candidate[1];
      span.dataset.selection = true;
      span.textContent = candidate[0];
      candidatePanel.appendChild(span);
    });
  }
};

window.addEventListener('load', function initIMEManager(evt) {
  window.removeEventListener('load', initIMEManager);
  IMEManager.init();
});

<|endoftext|>


<commit_before>const path = require('path');
const { findIndex, reduce, toString } = require('lodash');
const readDirP = require('readdirp-walk');
const { parseMarkdown } = require('../tools/challenge-md-parser');
const fs = require('fs');
/* eslint-disable max-len */
const {
  mergeChallenges,
  translateCommentsInChallenge
} = require('../tools/challenge-md-parser/translation-parser/translation-parser');
/* eslint-enable max-len*/
const { COMMENT_TRANSLATIONS } = require('./comment-dictionary');

const { isAuditedCert } = require('../utils/is-audited');
const { dasherize, nameify } = require('../utils/slugs');
const { createPoly } = require('../utils/polyvinyl');
const { blockNameify } = require('../utils/block-nameify');

const challengesDir = path.resolve(__dirname, './challenges');
const metaDir = path.resolve(challengesDir, '_meta');
exports.challengesDir = challengesDir;
exports.metaDir = metaDir;

function getChallengesDirForLang(lang) {
  return path.resolve(challengesDir, `./${lang}`);
}

function getMetaForBlock(block) {
  return JSON.parse(
    fs.readFileSync(path.resolve(metaDir, `./${block}/meta.json`), 'utf8')
  );
}

exports.getChallengesDirForLang = getChallengesDirForLang;
exports.getMetaForBlock = getMetaForBlock;

exports.getChallengesForLang = function getChallengesForLang(lang) {
  let curriculum = {};
  return new Promise(resolve => {
    let running = 1;
    function done() {
      if (--running === 0) {
        resolve(curriculum);
      }
    }
    readDirP({ root: getChallengesDirForLang(lang) })
      .on('data', file => {
        running++;
        buildCurriculum(file, curriculum).then(done);
      })
      .on('end', done);
  });
};

async function buildCurriculum(file, curriculum) {
  const { name, depth, path: filePath, fullPath, stat } = file;
  if (depth === 1 && stat.isDirectory()) {
    // extract the superBlock info
    const { order, name: superBlock } = superBlockInfo(name);
    curriculum[superBlock] = { superBlock, order, blocks: {} };
    return;
  }
  if (depth === 2 && stat.isDirectory()) {
    const blockName = getBlockNameFromPath(filePath);
    const metaPath = path.resolve(
      __dirname,
      `./challenges/_meta/${blockName}/meta.json`
    );
    const blockMeta = require(metaPath);
    const { isUpcomingChange } = blockMeta;
    if (typeof isUpcomingChange !== 'boolean') {
      throw Error(
        `meta file at ${metaPath} is missing 'isUpcomingChange', it must be 'true' or 'false'`
      );
    }

    if (!isUpcomingChange || process.env.SHOW_UPCOMING_CHANGES === 'true') {
      // add the block to the superBlock
      const { name: superBlock } = superBlockInfoFromPath(filePath);
      const blockInfo = { meta: blockMeta, challenges: [] };
      curriculum[superBlock].blocks[name] = blockInfo;
    }
    return;
  }
  if (name === 'meta.json' || name === '.DS_Store') {
    return;
  }

  const block = getBlockNameFromPath(filePath);
  const { name: superBlock } = superBlockInfoFromPath(filePath);
  let challengeBlock;

  // TODO: this try block and process exit can all go once errors terminate the
  // tests correctly.
  try {
    challengeBlock = curriculum[superBlock].blocks[block];
    if (!challengeBlock) {
      // this should only happen when a isUpcomingChange block is skipped
      return;
    }
  } catch (e) {
    console.log(`failed to create superBlock ${superBlock}`);
    // eslint-disable-next-line no-process-exit
    process.exit(1);
  }
  const { meta } = challengeBlock;

  const challenge = await createChallenge(fullPath, meta);

  challengeBlock.challenges = [...challengeBlock.challenges, challenge];
}

async function parseTranslation(engPath, transPath, dict) {
  const engChal = await parseMarkdown(engPath);
  const translatedChal = await parseMarkdown(transPath);

  const engWithTranslatedComments = translateCommentsInChallenge(
    engChal,
    getChallengeLang(transPath),
    dict
  );
  return mergeChallenges(engWithTranslatedComments, translatedChal);
}

exports.parseTranslation = parseTranslation;

async function createChallenge(fullPath, maybeMeta) {
  let meta;
  if (maybeMeta) {
    meta = maybeMeta;
  } else {
    const metaPath = path.resolve(
      metaDir,
      `./${getBlockNameFromFullPath(fullPath)}/meta.json`
    );
    meta = require(metaPath);
  }
  const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
  const lang = getChallengeLang(fullPath);
  if (!isAcceptedLanguage(lang))
    throw Error(`${lang} is not a accepted language.
Trying to parse ${fullPath}`);
  // assumes superblock names are unique
  // while the auditing is ongoing, we default to English for un-audited certs
  // once that's complete, we can revert to using isEnglishChallenge(fullPath)
  const isEnglish =
    isEnglishChallenge(fullPath) || !isAuditedCert(lang, superBlock);
  if (isEnglish) fullPath = getEnglishPath(fullPath);
  const challenge = await (isEnglish
    ? parseMarkdown(fullPath)
    : parseTranslation(
        getEnglishPath(fullPath),
        fullPath,
        COMMENT_TRANSLATIONS
      ));
  const challengeOrder = findIndex(
    meta.challengeOrder,
    ([id]) => id === challenge.id
  );
  const {
    name: blockName,
    order,
    superOrder,
    isPrivate,
    required = [],
    template,
    time
  } = meta;
  challenge.block = blockName;
  challenge.dashedName = dasherize(challenge.title);
  challenge.order = order;
  challenge.superOrder = superOrder;
  challenge.superBlock = superBlock;
  challenge.challengeOrder = challengeOrder;
  challenge.isPrivate = challenge.isPrivate || isPrivate;
  challenge.required = required.concat(challenge.required || []);
  challenge.template = template;
  challenge.time = time;

  return prepareChallenge(challenge);
}

// TODO: tests and more descriptive name.
function filesToObject(files) {
  return reduce(
    files,
    (map, file) => {
      map[file.key] = {
        ...file,
        head: arrToString(file.head),
        contents: arrToString(file.contents),
        tail: arrToString(file.tail)
      };
      return map;
    },
    {}
  );
}

// gets the challenge ready for sourcing into Gatsby
function prepareChallenge(challenge) {
  challenge.name = nameify(challenge.title);
  if (challenge.files) {
    challenge.files = filesToObject(challenge.files);
    // TODO: This should be something that can be folded into the above reduce
    // EDIT: maybe not, now that we're doing the same for solutionFiles.
    challenge.files = Object.keys(challenge.files)
      .filter(key => challenge.files[key])
      .map(key => challenge.files[key])
      .reduce(
        (files, file) => ({
          ...files,
          [file.key]: {
            ...createPoly(file),
            seed: file.contents.slice(0)
          }
        }),
        {}
      );
  }

  if (challenge.solutionFiles) {
    challenge.solutionFiles = filesToObject(challenge.solutionFiles);
  }
  challenge.block = dasherize(challenge.block);
  challenge.superBlock = blockNameify(challenge.superBlock);
  return challenge;
}

exports.createChallenge = createChallenge;

function getEnglishPath(fullPath) {
  const posix = path
    .normalize(fullPath)
    .split(path.sep)
    .join(path.posix.sep);
  const match = posix.match(/(.*curriculum\/challenges\/)([^/]*)(.*)(\2)(.*)/);
  const lang = getChallengeLang(fullPath);
  if (!isAcceptedLanguage(lang))
    throw Error(`${getChallengeLang(fullPath)} is not a accepted language.
Trying to parse ${fullPath}`);
  if (match) {
    return path.join(match[1], 'english', match[3] + 'english' + match[5]);
  } else {
    throw Error(`Malformed challenge path, ${fullPath} unable to parse.`);
  }
}

function getChallengeLang(fullPath) {
  const match = fullPath.match(/\.(\w+)\.md$/);
  if (!match || match.length < 2)
    throw Error(`Missing language extension for
${fullPath}`);
  return fullPath.match(/\.(\w+)\.md$/)[1];
}

function isEnglishChallenge(fullPath) {
  return getChallengeLang(fullPath) === 'english';
}

function isAcceptedLanguage(lang) {
  const acceptedLanguages = ['english', 'chinese'];
  return acceptedLanguages.includes(lang);
}

exports.getChallengeLang = getChallengeLang;
exports.getEnglishPath = getEnglishPath;
exports.isEnglishChallenge = isEnglishChallenge;

function superBlockInfoFromPath(filePath) {
  const [maybeSuper] = filePath.split(path.sep);
  return superBlockInfo(maybeSuper);
}

function superBlockInfoFromFullPath(fullFilePath) {
  const [, , maybeSuper] = fullFilePath.split(path.sep).reverse();
  return superBlockInfo(maybeSuper);
}

function superBlockInfo(fileName) {
  const [maybeOrder, ...superBlock] = fileName.split('-');
  let order = parseInt(maybeOrder, 10);
  if (isNaN(order)) {
    return { order: 0, name: fileName };
  } else {
    return {
      order: order,
      name: superBlock.join('-')
    };
  }
}

function getBlockNameFromPath(filePath) {
  const [, block] = filePath.split(path.sep);
  return block;
}

function getBlockNameFromFullPath(fullFilePath) {
  const [, block] = fullFilePath.split(path.sep).reverse();
  return block;
}

function arrToString(arr) {
  return Array.isArray(arr) ? arr.join('\n') : toString(arr);
}
<commit_msg>refactor: acceptedLanguage -> supported<commit_after>const path = require('path');
const { findIndex, reduce, toString } = require('lodash');
const readDirP = require('readdirp-walk');
const { parseMarkdown } = require('../tools/challenge-md-parser');
const fs = require('fs');
/* eslint-disable max-len */
const {
  mergeChallenges,
  translateCommentsInChallenge
} = require('../tools/challenge-md-parser/translation-parser/translation-parser');
/* eslint-enable max-len*/
const { COMMENT_TRANSLATIONS } = require('./comment-dictionary');

const { isAuditedCert } = require('../utils/is-audited');
const { dasherize, nameify } = require('../utils/slugs');
const { createPoly } = require('../utils/polyvinyl');
const { blockNameify } = require('../utils/block-nameify');
const { supportedLangs } = require('./utils');

const challengesDir = path.resolve(__dirname, './challenges');
const metaDir = path.resolve(challengesDir, '_meta');
exports.challengesDir = challengesDir;
exports.metaDir = metaDir;

function getChallengesDirForLang(lang) {
  return path.resolve(challengesDir, `./${lang}`);
}

function getMetaForBlock(block) {
  return JSON.parse(
    fs.readFileSync(path.resolve(metaDir, `./${block}/meta.json`), 'utf8')
  );
}

exports.getChallengesDirForLang = getChallengesDirForLang;
exports.getMetaForBlock = getMetaForBlock;

exports.getChallengesForLang = function getChallengesForLang(lang) {
  let curriculum = {};
  return new Promise(resolve => {
    let running = 1;
    function done() {
      if (--running === 0) {
        resolve(curriculum);
      }
    }
    readDirP({ root: getChallengesDirForLang(lang) })
      .on('data', file => {
        running++;
        buildCurriculum(file, curriculum).then(done);
      })
      .on('end', done);
  });
};

async function buildCurriculum(file, curriculum) {
  const { name, depth, path: filePath, fullPath, stat } = file;
  if (depth === 1 && stat.isDirectory()) {
    // extract the superBlock info
    const { order, name: superBlock } = superBlockInfo(name);
    curriculum[superBlock] = { superBlock, order, blocks: {} };
    return;
  }
  if (depth === 2 && stat.isDirectory()) {
    const blockName = getBlockNameFromPath(filePath);
    const metaPath = path.resolve(
      __dirname,
      `./challenges/_meta/${blockName}/meta.json`
    );
    const blockMeta = require(metaPath);
    const { isUpcomingChange } = blockMeta;
    if (typeof isUpcomingChange !== 'boolean') {
      throw Error(
        `meta file at ${metaPath} is missing 'isUpcomingChange', it must be 'true' or 'false'`
      );
    }

    if (!isUpcomingChange || process.env.SHOW_UPCOMING_CHANGES === 'true') {
      // add the block to the superBlock
      const { name: superBlock } = superBlockInfoFromPath(filePath);
      const blockInfo = { meta: blockMeta, challenges: [] };
      curriculum[superBlock].blocks[name] = blockInfo;
    }
    return;
  }
  if (name === 'meta.json' || name === '.DS_Store') {
    return;
  }

  const block = getBlockNameFromPath(filePath);
  const { name: superBlock } = superBlockInfoFromPath(filePath);
  let challengeBlock;

  // TODO: this try block and process exit can all go once errors terminate the
  // tests correctly.
  try {
    challengeBlock = curriculum[superBlock].blocks[block];
    if (!challengeBlock) {
      // this should only happen when a isUpcomingChange block is skipped
      return;
    }
  } catch (e) {
    console.log(`failed to create superBlock ${superBlock}`);
    // eslint-disable-next-line no-process-exit
    process.exit(1);
  }
  const { meta } = challengeBlock;

  const challenge = await createChallenge(fullPath, meta);

  challengeBlock.challenges = [...challengeBlock.challenges, challenge];
}

async function parseTranslation(engPath, transPath, dict) {
  const engChal = await parseMarkdown(engPath);
  const translatedChal = await parseMarkdown(transPath);

  const engWithTranslatedComments = translateCommentsInChallenge(
    engChal,
    getChallengeLang(transPath),
    dict
  );
  return mergeChallenges(engWithTranslatedComments, translatedChal);
}

exports.parseTranslation = parseTranslation;

async function createChallenge(fullPath, maybeMeta) {
  let meta;
  if (maybeMeta) {
    meta = maybeMeta;
  } else {
    const metaPath = path.resolve(
      metaDir,
      `./${getBlockNameFromFullPath(fullPath)}/meta.json`
    );
    meta = require(metaPath);
  }
  const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
  const lang = getChallengeLang(fullPath);
  if (!supportedLangs.includes(lang))
    throw Error(`${lang} is not a accepted language.
Trying to parse ${fullPath}`);
  // assumes superblock names are unique
  // while the auditing is ongoing, we default to English for un-audited certs
  // once that's complete, we can revert to using isEnglishChallenge(fullPath)
  const isEnglish =
    isEnglishChallenge(fullPath) || !isAuditedCert(lang, superBlock);
  if (isEnglish) fullPath = getEnglishPath(fullPath);
  const challenge = await (isEnglish
    ? parseMarkdown(fullPath)
    : parseTranslation(
        getEnglishPath(fullPath),
        fullPath,
        COMMENT_TRANSLATIONS
      ));
  const challengeOrder = findIndex(
    meta.challengeOrder,
    ([id]) => id === challenge.id
  );
  const {
    name: blockName,
    order,
    superOrder,
    isPrivate,
    required = [],
    template,
    time
  } = meta;
  challenge.block = blockName;
  challenge.dashedName = dasherize(challenge.title);
  challenge.order = order;
  challenge.superOrder = superOrder;
  challenge.superBlock = superBlock;
  challenge.challengeOrder = challengeOrder;
  challenge.isPrivate = challenge.isPrivate || isPrivate;
  challenge.required = required.concat(challenge.required || []);
  challenge.template = template;
  challenge.time = time;

  return prepareChallenge(challenge);
}

// TODO: tests and more descriptive name.
function filesToObject(files) {
  return reduce(
    files,
    (map, file) => {
      map[file.key] = {
        ...file,
        head: arrToString(file.head),
        contents: arrToString(file.contents),
        tail: arrToString(file.tail)
      };
      return map;
    },
    {}
  );
}

// gets the challenge ready for sourcing into Gatsby
function prepareChallenge(challenge) {
  challenge.name = nameify(challenge.title);
  if (challenge.files) {
    challenge.files = filesToObject(challenge.files);
    // TODO: This should be something that can be folded into the above reduce
    // EDIT: maybe not, now that we're doing the same for solutionFiles.
    challenge.files = Object.keys(challenge.files)
      .filter(key => challenge.files[key])
      .map(key => challenge.files[key])
      .reduce(
        (files, file) => ({
          ...files,
          [file.key]: {
            ...createPoly(file),
            seed: file.contents.slice(0)
          }
        }),
        {}
      );
  }

  if (challenge.solutionFiles) {
    challenge.solutionFiles = filesToObject(challenge.solutionFiles);
  }
  challenge.block = dasherize(challenge.block);
  challenge.superBlock = blockNameify(challenge.superBlock);
  return challenge;
}

exports.createChallenge = createChallenge;

function getEnglishPath(fullPath) {
  const posix = path
    .normalize(fullPath)
    .split(path.sep)
    .join(path.posix.sep);
  const match = posix.match(/(.*curriculum\/challenges\/)([^/]*)(.*)(\2)(.*)/);
  const lang = getChallengeLang(fullPath);
  if (!supportedLangs.includes(lang))
    throw Error(`${lang} is not a accepted language.
Trying to parse ${fullPath}`);
  if (match) {
    return path.join(match[1], 'english', match[3] + 'english' + match[5]);
  } else {
    throw Error(`Malformed challenge path, ${fullPath} unable to parse.`);
  }
}

function getChallengeLang(fullPath) {
  const match = fullPath.match(/\.(\w+)\.md$/);
  if (!match || match.length < 2)
    throw Error(`Missing language extension for
${fullPath}`);
  return fullPath.match(/\.(\w+)\.md$/)[1];
}

function isEnglishChallenge(fullPath) {
  return getChallengeLang(fullPath) === 'english';
}

exports.getChallengeLang = getChallengeLang;
exports.getEnglishPath = getEnglishPath;
exports.isEnglishChallenge = isEnglishChallenge;

function superBlockInfoFromPath(filePath) {
  const [maybeSuper] = filePath.split(path.sep);
  return superBlockInfo(maybeSuper);
}

function superBlockInfoFromFullPath(fullFilePath) {
  const [, , maybeSuper] = fullFilePath.split(path.sep).reverse();
  return superBlockInfo(maybeSuper);
}

function superBlockInfo(fileName) {
  const [maybeOrder, ...superBlock] = fileName.split('-');
  let order = parseInt(maybeOrder, 10);
  if (isNaN(order)) {
    return { order: 0, name: fileName };
  } else {
    return {
      order: order,
      name: superBlock.join('-')
    };
  }
}

function getBlockNameFromPath(filePath) {
  const [, block] = filePath.split(path.sep);
  return block;
}

function getBlockNameFromFullPath(fullFilePath) {
  const [, block] = fullFilePath.split(path.sep).reverse();
  return block;
}

function arrToString(arr) {
  return Array.isArray(arr) ? arr.join('\n') : toString(arr);
}
<|endoftext|>


<commit_before>var vm = require('vm');
var parseUrl = require('url').parse;
var VM_OPTIONS = {
    displayErrors: false,
    timeout: 100,
    filename: 'proxy.pac'
  };
var dateRange = require('./dateRange');
var dnsDomainIs = require('./dnsDomainIs');
var dnsDomainLevels = require('./dnsDomainLevels');
var getDnsResolve = require('./dnsResolve');
var isInNet = require('./isInNet');
var isPlainHostName = require('./isPlainHostName');
var isResolvable = require('./isResolvable');
var localHostOrDomainIs = require('./localHostOrDomainIs');
var myIpAddress = require('./myIpAddress');
var shExpMatch = require('./shExpMatch');
var timeRange = require('./timeRange');
var weekdayRange = require('./weekdayRange');

function generateFn(script, dnsResolve) {
  try {
    var fn = vm.runInNewContext(script + ';FindProxyForURL', {
      dateRange: dateRange,
      dnsDomainIs: dnsDomainIs,
      dnsDomainLevels: dnsDomainLevels,
      dnsResolve: getDnsResolve(dnsResolve),
      isInNet: isInNet,
      isPlainHostName: isPlainHostName,
      isResolvable: isResolvable,
      localHostOrDomainIs: localHostOrDomainIs,
      myIpAddress: myIpAddress,
      shExpMatch: shExpMatch,
      timeRange: timeRange,
      weekdayRange: weekdayRange
    }, VM_OPTIONS);
    return typeof fn === 'function' ? fn : null;
  } catch(e) {}
}

var DIRECT = 'DIRECT';
var directPac = function(url, host, cb) {
  cb(null, DIRECT);
};
var emptyDnsResolve = function(host, cb) {
  cb();
};
module.exports = function(script, dnsResolve) {
  var _FindProxyForURL = generateFn(script, dnsResolve) || directPac;
  if (typeof dnsResolve !== 'function') {
    dnsResolve = emptyDnsResolve;
  }
  return function FindProxyForURL(url, host, cb) {
    var findProxyForURLSync = function() {
      var result;
      try {
        host = host || parseUrl(url).hostname;
        result = _FindProxyForURL(url, host);
        result = typeof result == 'string' ? result : DIRECT;
      } catch(e) {}
      return result || DIRECT;
    };
    if (typeof cb !== 'function') {
      return findProxyForURLSync();
    }
    dnsResolve(host, function() {
      cb(null, findProxyForURLSync());
    });
  };
};
<commit_msg>feat: Support custom dnsResolve<commit_after>var vm = require('vm');
var parseUrl = require('url').parse;
var VM_OPTIONS = {
    displayErrors: false,
    timeout: 100,
    filename: 'proxy.pac'
  };
var dateRange = require('./dateRange');
var dnsDomainIs = require('./dnsDomainIs');
var dnsDomainLevels = require('./dnsDomainLevels');
var getDnsResolve = require('./dnsResolve');
var isInNet = require('./isInNet');
var isPlainHostName = require('./isPlainHostName');
var isResolvable = require('./isResolvable');
var localHostOrDomainIs = require('./localHostOrDomainIs');
var myIpAddress = require('./myIpAddress');
var shExpMatch = require('./shExpMatch');
var timeRange = require('./timeRange');
var weekdayRange = require('./weekdayRange');

function generateFn(script, dnsResolve) {
  try {
    var fn = vm.runInNewContext(script + ';FindProxyForURL', {
      dateRange: dateRange,
      dnsDomainIs: dnsDomainIs,
      dnsDomainLevels: dnsDomainLevels,
      dnsResolve: getDnsResolve(dnsResolve),
      isInNet: isInNet,
      isPlainHostName: isPlainHostName,
      isResolvable: isResolvable,
      localHostOrDomainIs: localHostOrDomainIs,
      myIpAddress: myIpAddress,
      shExpMatch: shExpMatch,
      timeRange: timeRange,
      weekdayRange: weekdayRange
    }, VM_OPTIONS);
    return typeof fn === 'function' ? fn : null;
  } catch(e) {}
}

var DIRECT = 'DIRECT';
var directPac = function(url, host, cb) {
  cb(null, DIRECT);
};
var emptyDnsResolve = function(host, cb) {
  cb();
};
module.exports = function(script, dnsResolve) {
  var _FindProxyForURL = generateFn(script, dnsResolve) || directPac;
  if (typeof dnsResolve !== 'function') {
    dnsResolve = emptyDnsResolve;
  }
  return function FindProxyForURL(url, host, cb) {
    var result;
    dnsResolve(host,  function() {
      try {
        host = host || parseUrl(url).hostname;
        result = _FindProxyForURL(url, host);
        result = typeof result == 'string' ? result : DIRECT;
      } catch(e) {}
      if (typeof cb == 'function') {
        cb(null, result || DIRECT);
      }
    });
    return result || DIRECT;
  };
};
<|endoftext|>


<commit_before>const path = require('path');
const electron = require('electron');
const NSEventMonitor = require('nseventmonitor');
const { app, BrowserWindow, Tray } = electron;

let window = null;
let tray = null;
let macEventMonitor = null;

// hide dock icon
app.dock.hide();

const getWindowPosition = () => {
  const windowBounds = window.getBounds();
  const trayBounds = tray.getBounds();

  // center window horizontally below the tray icon
  const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2));

  // position window vertically below the tray icon
  const y = Math.round(trayBounds.y + trayBounds.height);

  return { x, y };
};

const createWindow = () => {
  window = new BrowserWindow({
    width: 320,
    height: 400,
    frame: false,
    resizable: false,
    maximizable: false,
    fullscreenable: false,
    transparent: true,
    show: false,
    webPreferences: {
      // prevents renderer process code from not running when window is hidden
      backgroundThrottling: false
    }
  });

  macEventMonitor = new NSEventMonitor();

  window.loadURL('file://' + path.join(__dirname, 'index.html'));

  window.on('blur', () => {
    window.hide();
  });

  window.on('show', () => {
    tray.setHighlightMode('always');

    // start capturing global mouse events
    macEventMonitor.start(() => {
      window.hide();
    });
  });

  window.on('hide', () => {
    tray.setHighlightMode('never');

    // stop capturing global mouse events
    macEventMonitor.stop();
  });

};

const toggleWindow = () => {
  if(window.isVisible()) {
    window.hide();
  } else {
    showWindow();
  }
};

const showWindow = () => {
  const position = getWindowPosition();
  window.setPosition(position.x, position.y, false);
  window.show();
  window.focus();
};

const createTray = () => {
  tray = new Tray(path.join(__dirname, 'trayIconTemplate.png'));
  tray.on('right-click', toggleWindow);
  tray.on('double-click', toggleWindow);
  tray.on('click', toggleWindow);
};

app.on('window-all-closed', () => {
  app.quit();
});

app.on('ready', () => {
  createTray();
  createWindow();
});
<commit_msg>Update example<commit_after>const path = require('path');
const electron = require('electron');
const { NSEventMonitor, NSEventMask } = require('nseventmonitor');
const { app, BrowserWindow, Tray } = electron;

let window = null;
let tray = null;
let macEventMonitor = null;

// hide dock icon
app.dock.hide();

const getWindowPosition = () => {
  const windowBounds = window.getBounds();
  const trayBounds = tray.getBounds();

  // center window horizontally below the tray icon
  const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2));

  // position window vertically below the tray icon
  const y = Math.round(trayBounds.y + trayBounds.height);

  return { x, y };
};

const createWindow = () => {
  window = new BrowserWindow({
    width: 320,
    height: 400,
    frame: false,
    resizable: false,
    maximizable: false,
    fullscreenable: false,
    transparent: true,
    show: false,
    webPreferences: {
      // prevents renderer process code from not running when window is hidden
      backgroundThrottling: false
    }
  });

  macEventMonitor = new NSEventMonitor();

  window.loadURL('file://' + path.join(__dirname, 'index.html'));

  window.on('blur', () => {
    window.hide();
  });

  window.on('show', () => {
    tray.setHighlightMode('always');

    // start capturing global mouse events
    macEventMonitor.start((NSEventMask.leftMouseDown | NSEventMask.rightMouseDown), () => {
      window.hide();
    });
  });

  window.on('hide', () => {
    tray.setHighlightMode('never');

    // stop capturing global mouse events
    macEventMonitor.stop();
  });

};

const toggleWindow = () => {
  if(window.isVisible()) {
    window.hide();
  } else {
    showWindow();
  }
};

const showWindow = () => {
  const position = getWindowPosition();
  window.setPosition(position.x, position.y, false);
  window.show();
  window.focus();
};

const createTray = () => {
  tray = new Tray(path.join(__dirname, 'trayIconTemplate.png'));
  tray.on('right-click', toggleWindow);
  tray.on('double-click', toggleWindow);
  tray.on('click', toggleWindow);
};

app.on('window-all-closed', () => {
  app.quit();
});

app.on('ready', () => {
  createTray();
  createWindow();
});
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @flow

import { resourceTypes } from './data-structures';
import { SymbolsNotFoundError } from './errors';

import type {
  Profile,
  Thread,
  ThreadIndex,
  Lib,
  IndexIntoFuncTable,
} from '../types/profile';
import type { MemoryOffset } from '../types/units';
import type {
  AbstractSymbolStore,
  AddressResult,
  LibSymbolicationRequest,
} from './symbol-store';

type SymbolicationHandlers = {
  onMergeFunctions: (
    threadIndex: ThreadIndex,
    Map<IndexIntoFuncTable, IndexIntoFuncTable>
  ) => void,
  onGotFuncNames: (
    threadIndex: ThreadIndex,
    funcIndices: IndexIntoFuncTable[],
    funcNames: string[]
  ) => void,
};

type LibKey = string; // of the form ${debugName}/${breakpadId}
type Address = number;
type ThreadSymbolicationInfo = Map<LibKey, Map<Address, IndexIntoFuncTable>>;

/**
 * Return the library object that contains the address such that
 * rv.start <= address < rv.end, or null if no such lib object exists.
 */
export function getContainingLibrary(
  libs: Lib[],
  address: MemoryOffset
): Lib | null {
  if (isNaN(address)) {
    return null;
  }

  let left = 0;
  let right = libs.length - 1;
  while (left <= right) {
    const mid = ((left + right) / 2) | 0;
    if (address >= libs[mid].end) {
      left = mid + 1;
    } else if (address < libs[mid].start) {
      right = mid - 1;
    } else {
      return libs[mid];
    }
  }
  return null;
}

/**
 * Find the functions in this thread's funcTable that we need symbols for. The map
 * that is returned is keyed off the Lib objects, and has a list of IndexIntoFuncTable
 * for the still unsymbolicated functions.
 */
function gatherFuncsInThread(thread: Thread): Map<Lib, IndexIntoFuncTable[]> {
  const { libs, funcTable, stringTable, resourceTable } = thread;
  const foundAddresses: Map<Lib, IndexIntoFuncTable[]> = new Map();
  for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) {
    const resourceIndex = funcTable.resource[funcIndex];
    if (resourceIndex === -1) {
      continue;
    }
    const resourceType = resourceTable.type[resourceIndex];
    if (resourceType !== resourceTypes.library) {
      continue;
    }

    const name = stringTable.getString(funcTable.name[funcIndex]);
    if (!name.startsWith('0x')) {
      // Somebody already symbolicated this function for us.
      continue;
    }

    const libIndex = resourceTable.lib[resourceIndex];
    if (libIndex === null || libIndex === undefined || libIndex === -1) {
      throw new Error('libIndex must be a valid index.');
    }
    const lib = libs[libIndex];
    if (lib === undefined) {
      throw new Error('Did not find a lib.');
    }
    let libFuncs = foundAddresses.get(lib);
    if (libFuncs === undefined) {
      libFuncs = [];
      foundAddresses.set(lib, libFuncs);
    }
    libFuncs.push(funcIndex);
  }
  return foundAddresses;
}

/**
 * Modify the symbolicated funcs to point to the new func name strings.
 * This function adds the func names to the thread's string table and
 * adjusts the funcTable to point to those strings.
 */
export function setFuncNames(
  thread: Thread,
  funcIndices: IndexIntoFuncTable[],
  funcNames: string[]
): Thread {
  const funcTable = Object.assign({}, thread.funcTable);
  funcTable.name = funcTable.name.slice();
  const stringTable = thread.stringTable;
  funcIndices.forEach((funcIndex, i) => {
    const symbolName = funcNames[i];
    const symbolIndex = stringTable.indexForString(symbolName);
    funcTable.name[funcIndex] = symbolIndex;
  });
  return { ...thread, funcTable, stringTable };
}

/**
 * Correctly collapse a function into another function and return a consistent
 * profile that no longer refers to the collapsed-away function.
 * The new frameTable has an updated func column, where all indices
 * to old funcs have been replaced to the corresponding new func.
 * "Functions" in a profile are created before the library's function table is
 * known, by creating one function per frame address. Once the function table
 * is known, different addresses that are inside the same function need to be
 * merged into that same function.
 */
export function applyFunctionMerging(
  thread: Thread,
  oldFuncToNewFuncMap: Map<IndexIntoFuncTable, IndexIntoFuncTable>
): Thread {
  const frameTable = Object.assign({}, thread.frameTable, {
    func: thread.frameTable.func.map(oldFunc => {
      const newFunc = oldFuncToNewFuncMap.get(oldFunc);
      return newFunc === undefined ? oldFunc : newFunc;
    }),
  });
  return { ...thread, frameTable };
}

// Gather the symbols needed in this thread into a nested map:
// libKey -> address relative to lib -> index of the func
function getThreadSymbolicationInfo(thread: Thread): ThreadSymbolicationInfo {
  const foundFuncMap = gatherFuncsInThread(thread);
  const { funcTable } = thread;
  const libKeyToAddressToFuncMap = new Map();
  for (const [lib, funcsToSymbolicate] of foundFuncMap) {
    const libKey = `${lib.debugName}/${lib.breakpadId}`;
    const addressToFuncMap = new Map();
    libKeyToAddressToFuncMap.set(libKey, addressToFuncMap);
    for (const funcIndex of funcsToSymbolicate) {
      const funcAddress = funcTable.address[funcIndex];
      addressToFuncMap.set(funcAddress, funcIndex);
    }
  }
  return libKeyToAddressToFuncMap;
}

// Go through all the threads to gather up the addresses we need to symbolicate
// for each library.
function buildLibSymbolicationRequestsForAllThreads(
  symbolicationInfo: ThreadSymbolicationInfo[]
): LibSymbolicationRequest[] {
  const libKeyToAddressesMap = new Map();
  for (const threadSymbolicationInfo of symbolicationInfo) {
    for (const [libKey, addressToFuncMap] of threadSymbolicationInfo) {
      let addressSet = libKeyToAddressesMap.get(libKey);
      if (addressSet === undefined) {
        addressSet = new Set();
        libKeyToAddressesMap.set(libKey, addressSet);
      }
      for (const funcAddress of addressToFuncMap.keys()) {
        addressSet.add(funcAddress);
      }
    }
  }
  return Array.from(libKeyToAddressesMap).map(([libKey, addresses]) => {
    const [debugName, breakpadId] = libKey.split('/');
    const lib = { debugName, breakpadId };
    return { lib, addresses };
  });
}

// With the symbolication results for the library given by libKey, call the
// symbolicationHandlers for each thread. Those calls will ensure that the
// symbolication information eventually makes it into the thread.
// Most of the work in this function is just munging the data that we have into
// the shape that the symbolicationHandlers methods expect.
function finishSymbolicationForLib(
  profile: Profile,
  symbolicationInfo: ThreadSymbolicationInfo[],
  resultsForLib: Map<number, AddressResult>,
  libKey: string,
  symbolicationHandlers: SymbolicationHandlers
): void {
  const { threads } = profile;
  for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
    const threadSymbolicationInfo = symbolicationInfo[threadIndex];
    const addressToFuncIndexMap = threadSymbolicationInfo.get(libKey);
    if (addressToFuncIndexMap !== undefined) {
      const funcAddressToCanonicalFuncIndexMap = new Map();
      const funcIndices = [];
      const funcNames = [];
      const oldFuncToNewFuncMap = new Map();
      for (const [address, funcIndex] of addressToFuncIndexMap) {
        const addressResult = resultsForLib.get(address);
        if (addressResult !== undefined) {
          // |address| is the original address that we found during stackwalking,
          // as a library-relative offset.
          // |addressResult.functionOffset| is the offset between the start of
          // the function and |address|.
          // |funcAddress| is the start of the function, as a library-relative
          // offset.
          const funcAddress = address - addressResult.functionOffset;
          const canonicalFuncIndexForThisFuncAddress = funcAddressToCanonicalFuncIndexMap.get(
            funcAddress
          );
          if (canonicalFuncIndexForThisFuncAddress !== undefined) {
            oldFuncToNewFuncMap.set(
              funcIndex,
              canonicalFuncIndexForThisFuncAddress
            );
          } else {
            funcAddressToCanonicalFuncIndexMap.set(funcAddress, funcIndex);
            funcIndices.push(funcIndex);
            funcNames.push(addressResult.name);
          }
        }
      }
      symbolicationHandlers.onMergeFunctions(threadIndex, oldFuncToNewFuncMap);
      symbolicationHandlers.onGotFuncNames(threadIndex, funcIndices, funcNames);
    }
  }
}

/**
 * When collecting profile samples, the profiler only collects raw memory addresses
 * of the program's functions. This function takes the list of memory addresses, and
 * uses a symbol store to look up the symbols (names) of all of the functions. Initially
 * each memory address is a assigned a function in the profile, but these addresses may
 * actually point to the same function. During the processes of symbolication, any memory
 * addresses that share the same function have their FrameTable and FuncTable updated
 * to point to same function.
 */
export async function symbolicateProfile(
  profile: Profile,
  symbolStore: AbstractSymbolStore,
  symbolicationHandlers: SymbolicationHandlers
): Promise<void> {
  const symbolicationInfo = profile.threads.map(getThreadSymbolicationInfo);
  const libSymbolicationRequests = buildLibSymbolicationRequestsForAllThreads(
    symbolicationInfo
  );
  await symbolStore.getSymbols(
    libSymbolicationRequests,
    (request, results) => {
      const { debugName, breakpadId } = request.lib;
      const libKey = `${debugName}/${breakpadId}`;
      finishSymbolicationForLib(
        profile,
        symbolicationInfo,
        results,
        libKey,
        symbolicationHandlers
      );
    },
    (request, error) => {
      if (!(error instanceof SymbolsNotFoundError)) {
        // rethrow JavaScript programming error
        throw error;
      }
      // We could not find symbols for this library.
      console.warn(error);
    }
  );
}
<commit_msg>Improve in-code symbolication documentation.<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @flow

import { resourceTypes } from './data-structures';
import { SymbolsNotFoundError } from './errors';

import type {
  Profile,
  Thread,
  ThreadIndex,
  Lib,
  IndexIntoFuncTable,
} from '../types/profile';
import type { MemoryOffset } from '../types/units';
import type {
  AbstractSymbolStore,
  AddressResult,
  LibSymbolicationRequest,
} from './symbol-store';

// Contains functions to symbolicate a profile.

/**
 * Symbolication Overview
 *
 * Symbolication is the process of looking up function names for native code and
 * assigning those function names to the functions in the profile.
 *
 * When the profiler samples call stacks for native code, it only collects
 * addresses: the address of the instruction which is currently being executed,
 * and the "return addresses" for its callers on the stack, i.e. the addresses
 * of the instructions that will be executed after each function returns.
 * To obtain a profile with function names, these addresses need to be
 * translated into library-relative offsets, looked up per library, and then
 * substituted with the corresponding function name strings in the profile.
 *
 * The actual lookup of symbols is not handled in this file; it is delegated to
 * an AbstractSymbolStore interface.
 *
 * The functions in this file perform the following tasks:
 *  - assigning addresses to their containing libraries so that they can be
 *    translated into library-relative offsets
 *  - gathering all addresses in the profile which require symbolication,
 *    grouped by library, and requesting symbols from the symbol store
 *  - once the results from the symbol store come in, performing the
 *    substutitions in the profile.
 *
 * Implementation details
 *
 * The implementation has the following constraints:
 *  - Symbolication needs to be asynchronous, and the profile needs to be fully
 *    interactive before symbols have arrived. This means that there needs to
 *    be an intact funcTable from the very start, because the call nodes for
 *    the call tree are based on funcs.
 *  - When the symbols arrive, we cannot mutate the profile in-place. Instead,
 *    we need to delegate the profile mutation to our caller so that it can go
 *    through redux actions and reducers; the profile is part of the redux state
 *    so any profile adjustment needs to follow proper procedure. This allows
 *    all derived data and UI that depends on profile contents to be notified
 *    and updated in the regular ways.
 *    It also allows multiple steps of symbolication to happen in one redux
 *    store update, which saves re-renders if symbol results for many small
 *    libraries arrive at nearly the same time.
 *  - When symbols arrive, the user shouldn't "lose their place" in the call
 *    tree UI; concretely this means that the selected call node and the set of
 *    expanded call nodes should survive symbolication-triggered adjustments of
 *    the funcTable as much as possible. More specifically, if all frames that
 *    used to be assigned to function A get reassigned to function B, we want to
 *    create an A -> B entry in an oldFuncToNewFuncMap that gets dispatched in
 *    a redux action so that the appropriate parts of the redux state can react.
 *  - Multiple processes and threads in the profile will require symbols from
 *    the same set of native libraries, and the number and size of the
 *    symbolication requests for each library should be minimized. This means
 *    that we want to gather all needed addresses across the entire profile
 *    first, rather than requesting symbols separately for each thread.
 *
 * Symbolication happens in a number of phases.
 *
 * I. Preparation during profile processing: When native code addresses initially
 * arrive in the Firefox profiler, they come in the form of hex strings in the
 * frame table of the Gecko profile. Profile processing then does the following:
 *  - It detects frames of native code based on the hex string format.
 *  - It looks up the containing library for each address.
 *  - For each native frame, it creates one funcTable entry. It can't really do
 *    any better at this point because it would need to have the symbols in
 *    order to create only one func per actual function. So, to repeat, the
 *    initial funcTable has one func per native frame.
 *  - The frame and its func both get their address field set to the
 *    library-relative offset.
 *  - The func's resource field is set to a resource of type "library" that
 *    points to the lib object in the thread's "libs" list that contained this
 *    address. The frame's and func's address fields are relative to that lib.
 *
 * II. Address gathering per library: This step goes through all threads and
 * gathers up addresses per library that need to be symbolicated. It also keeps
 * around enough per-thread information so that the per-thread substitution step
 * at the end can perform its work efficiently.
 *
 * III. Symbol lookup: Handled by the AbstractSymbolStore.
 *
 * IV. Symbol result processing: Does enough processing of the result to create
 * the oldFuncToNewFuncMap, and prepares data for the final substitution step.
 *
 * V. Profile substitution: Invoked from the redux reducer. Creates a new thread
 * object with an updated frameTable, funcTable and stringTable, with the
 * symbols substituted in the right places. This usually causes many funcs to
 * be orphaned (no frames will use them any more); these orphaned funcs remain
 * in the funcTable.
 */

type SymbolicationHandlers = {
  onMergeFunctions: (
    threadIndex: ThreadIndex,
    Map<IndexIntoFuncTable, IndexIntoFuncTable>
  ) => void,
  onGotFuncNames: (
    threadIndex: ThreadIndex,
    funcIndices: IndexIntoFuncTable[],
    funcNames: string[]
  ) => void,
};

type LibKey = string; // of the form ${debugName}/${breakpadId}
type Address = number;
type ThreadSymbolicationInfo = Map<LibKey, Map<Address, IndexIntoFuncTable>>;

/**
 * Return the library object that contains the address such that
 * rv.start <= address < rv.end, or null if no such lib object exists.
 * The libs array needs to be sorted in ascending address order, and the address
 * ranges of the libraries need to be non-overlapping.
 */
export function getContainingLibrary(
  libs: Lib[],
  address: MemoryOffset
): Lib | null {
  if (isNaN(address)) {
    return null;
  }

  let left = 0;
  let right = libs.length - 1;
  while (left <= right) {
    const mid = ((left + right) / 2) | 0;
    if (address >= libs[mid].end) {
      left = mid + 1;
    } else if (address < libs[mid].start) {
      right = mid - 1;
    } else {
      return libs[mid];
    }
  }
  return null;
}

/**
 * Find the functions in this thread's funcTable that we need symbols for. The map
 * that is returned is keyed off the Lib objects, and has a list of IndexIntoFuncTable
 * for the still unsymbolicated functions.
 */
function gatherFuncsInThread(thread: Thread): Map<Lib, IndexIntoFuncTable[]> {
  const { libs, funcTable, stringTable, resourceTable } = thread;
  const foundAddresses: Map<Lib, IndexIntoFuncTable[]> = new Map();
  for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) {
    const resourceIndex = funcTable.resource[funcIndex];
    if (resourceIndex === -1) {
      continue;
    }
    const resourceType = resourceTable.type[resourceIndex];
    if (resourceType !== resourceTypes.library) {
      continue;
    }

    const name = stringTable.getString(funcTable.name[funcIndex]);
    if (!name.startsWith('0x')) {
      // Somebody already symbolicated this function for us.
      continue;
    }

    const libIndex = resourceTable.lib[resourceIndex];
    if (libIndex === null || libIndex === undefined || libIndex === -1) {
      throw new Error('libIndex must be a valid index.');
    }
    const lib = libs[libIndex];
    if (lib === undefined) {
      throw new Error('Did not find a lib.');
    }
    let libFuncs = foundAddresses.get(lib);
    if (libFuncs === undefined) {
      libFuncs = [];
      foundAddresses.set(lib, libFuncs);
    }
    libFuncs.push(funcIndex);
  }
  return foundAddresses;
}

/**
 * Modify the symbolicated funcs to point to the new func name strings.
 * This function adds the func names to the thread's string table and
 * adjusts the funcTable to point to those strings.
 */
export function setFuncNames(
  thread: Thread,
  funcIndices: IndexIntoFuncTable[],
  funcNames: string[]
): Thread {
  const funcTable = Object.assign({}, thread.funcTable);
  funcTable.name = funcTable.name.slice();
  const stringTable = thread.stringTable;
  funcIndices.forEach((funcIndex, i) => {
    const symbolName = funcNames[i];
    const symbolIndex = stringTable.indexForString(symbolName);
    funcTable.name[funcIndex] = symbolIndex;
  });
  return { ...thread, funcTable, stringTable };
}

/**
 * Correctly collapse a function into another function and return a consistent
 * profile that no longer refers to the collapsed-away function.
 * The new frameTable has an updated func column, where all indices
 * to old funcs have been replaced to the corresponding new func.
 * "Functions" in a profile are created before the library's function table is
 * known, by creating one function per frame address. Once the function table
 * is known, different addresses that are inside the same function need to be
 * merged into that same function.
 */
export function applyFunctionMerging(
  thread: Thread,
  oldFuncToNewFuncMap: Map<IndexIntoFuncTable, IndexIntoFuncTable>
): Thread {
  const frameTable = Object.assign({}, thread.frameTable, {
    func: thread.frameTable.func.map(oldFunc => {
      const newFunc = oldFuncToNewFuncMap.get(oldFunc);
      return newFunc === undefined ? oldFunc : newFunc;
    }),
  });
  return { ...thread, frameTable };
}

// Gather the symbols needed in this thread into a nested map:
// libKey -> address relative to lib -> index of the func
function getThreadSymbolicationInfo(thread: Thread): ThreadSymbolicationInfo {
  const foundFuncMap = gatherFuncsInThread(thread);
  const { funcTable } = thread;
  const libKeyToAddressToFuncMap = new Map();
  for (const [lib, funcsToSymbolicate] of foundFuncMap) {
    const libKey = `${lib.debugName}/${lib.breakpadId}`;
    const addressToFuncMap = new Map();
    libKeyToAddressToFuncMap.set(libKey, addressToFuncMap);
    for (const funcIndex of funcsToSymbolicate) {
      const funcAddress = funcTable.address[funcIndex];
      addressToFuncMap.set(funcAddress, funcIndex);
    }
  }
  return libKeyToAddressToFuncMap;
}

// Go through all the threads to gather up the addresses we need to symbolicate
// for each library.
function buildLibSymbolicationRequestsForAllThreads(
  symbolicationInfo: ThreadSymbolicationInfo[]
): LibSymbolicationRequest[] {
  const libKeyToAddressesMap = new Map();
  for (const threadSymbolicationInfo of symbolicationInfo) {
    for (const [libKey, addressToFuncMap] of threadSymbolicationInfo) {
      let addressSet = libKeyToAddressesMap.get(libKey);
      if (addressSet === undefined) {
        addressSet = new Set();
        libKeyToAddressesMap.set(libKey, addressSet);
      }
      for (const funcAddress of addressToFuncMap.keys()) {
        addressSet.add(funcAddress);
      }
    }
  }
  return Array.from(libKeyToAddressesMap).map(([libKey, addresses]) => {
    const [debugName, breakpadId] = libKey.split('/');
    const lib = { debugName, breakpadId };
    return { lib, addresses };
  });
}

// With the symbolication results for the library given by libKey, call the
// symbolicationHandlers for each thread. Those calls will ensure that the
// symbolication information eventually makes it into the thread.
// Most of the work in this function is just munging the data that we have into
// the shape that the symbolicationHandlers methods expect.
function finishSymbolicationForLib(
  profile: Profile,
  symbolicationInfo: ThreadSymbolicationInfo[],
  resultsForLib: Map<number, AddressResult>,
  libKey: string,
  symbolicationHandlers: SymbolicationHandlers
): void {
  const { threads } = profile;
  for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) {
    const threadSymbolicationInfo = symbolicationInfo[threadIndex];
    const addressToFuncIndexMap = threadSymbolicationInfo.get(libKey);
    if (addressToFuncIndexMap !== undefined) {
      const funcAddressToCanonicalFuncIndexMap = new Map();
      const funcIndices = [];
      const funcNames = [];
      const oldFuncToNewFuncMap = new Map();
      for (const [address, funcIndex] of addressToFuncIndexMap) {
        const addressResult = resultsForLib.get(address);
        if (addressResult !== undefined) {
          // |address| is the original address that we found during stackwalking,
          // as a library-relative offset.
          // |addressResult.functionOffset| is the offset between the start of
          // the function and |address|.
          // |funcAddress| is the start of the function, as a library-relative
          // offset.
          const funcAddress = address - addressResult.functionOffset;
          const canonicalFuncIndexForThisFuncAddress = funcAddressToCanonicalFuncIndexMap.get(
            funcAddress
          );
          if (canonicalFuncIndexForThisFuncAddress !== undefined) {
            oldFuncToNewFuncMap.set(
              funcIndex,
              canonicalFuncIndexForThisFuncAddress
            );
          } else {
            funcAddressToCanonicalFuncIndexMap.set(funcAddress, funcIndex);
            funcIndices.push(funcIndex);
            funcNames.push(addressResult.name);
          }
        }
      }
      symbolicationHandlers.onMergeFunctions(threadIndex, oldFuncToNewFuncMap);
      symbolicationHandlers.onGotFuncNames(threadIndex, funcIndices, funcNames);
    }
  }
}

/**
 * When collecting profile samples, the profiler only collects raw memory addresses
 * of the program's functions. This function takes the list of memory addresses, and
 * uses a symbol store to look up the symbols (names) of all of the functions. Initially
 * each memory address is a assigned a function in the profile, but these addresses may
 * actually point to the same function. During the processes of symbolication, any memory
 * addresses that share the same function have their FrameTable and FuncTable updated
 * to point to same function.
 */
export async function symbolicateProfile(
  profile: Profile,
  symbolStore: AbstractSymbolStore,
  symbolicationHandlers: SymbolicationHandlers
): Promise<void> {
  const symbolicationInfo = profile.threads.map(getThreadSymbolicationInfo);
  const libSymbolicationRequests = buildLibSymbolicationRequestsForAllThreads(
    symbolicationInfo
  );
  await symbolStore.getSymbols(
    libSymbolicationRequests,
    (request, results) => {
      const { debugName, breakpadId } = request.lib;
      const libKey = `${debugName}/${breakpadId}`;
      finishSymbolicationForLib(
        profile,
        symbolicationInfo,
        results,
        libKey,
        symbolicationHandlers
      );
    },
    (request, error) => {
      if (!(error instanceof SymbolsNotFoundError)) {
        // rethrow JavaScript programming error
        throw error;
      }
      // We could not find symbols for this library.
      console.warn(error);
    }
  );
}
<|endoftext|>