src/menu-helpers.js

Summary

Maintainability
A
1 hr
Test Coverage
const _ = require('underscore-plus');

const ItemSpecificities = new WeakMap();

// Add an item to a menu, ensuring separators are not duplicated.
function addItemToMenu(item, menu) {
  const lastMenuItem = _.last(menu);
  const lastMenuItemIsSpearator =
    lastMenuItem && lastMenuItem.type === 'separator';
  if (!(item.type === 'separator' && lastMenuItemIsSpearator)) {
    menu.push(item);
  }
}

function merge(menu, item, itemSpecificity = Infinity) {
  item = cloneMenuItem(item);
  ItemSpecificities.set(item, itemSpecificity);
  const matchingItemIndex = findMatchingItemIndex(menu, item);

  if (matchingItemIndex === -1) {
    addItemToMenu(item, menu);
    return;
  }

  const matchingItem = menu[matchingItemIndex];
  if (item.submenu != null) {
    for (let submenuItem of item.submenu) {
      merge(matchingItem.submenu, submenuItem, itemSpecificity);
    }
  } else if (
    itemSpecificity &&
    itemSpecificity >= ItemSpecificities.get(matchingItem)
  ) {
    menu[matchingItemIndex] = item;
  }
}

function unmerge(menu, item) {
  item = cloneMenuItem(item);
  const matchingItemIndex = findMatchingItemIndex(menu, item);
  if (matchingItemIndex === -1) {
    return;
  }

  const matchingItem = menu[matchingItemIndex];
  if (item.submenu != null) {
    for (let submenuItem of item.submenu) {
      unmerge(matchingItem.submenu, submenuItem);
    }
  }

  if (matchingItem.submenu == null || matchingItem.submenu.length === 0) {
    menu.splice(matchingItemIndex, 1);
  }
}

function findMatchingItemIndex(menu, { type, id, submenu }) {
  if (type === 'separator') {
    return -1;
  }
  for (let index = 0; index < menu.length; index++) {
    const item = menu[index];
    if (item.id === id && (item.submenu != null) === (submenu != null)) {
      return index;
    }
  }
  return -1;
}

function normalizeLabel(label) {
  if (label == null) {
    return;
  }
  return process.platform === 'darwin' ? label : label.replace(/&/g, '');
}

function cloneMenuItem(item) {
  item = _.pick(
    item,
    'type',
    'label',
    'id',
    'enabled',
    'visible',
    'command',
    'submenu',
    'commandDetail',
    'role',
    'accelerator',
    'before',
    'after',
    'beforeGroupContaining',
    'afterGroupContaining'
  );
  if (item.id === null || item.id === undefined) {
    item.id = normalizeLabel(item.label);
  }
  if (item.submenu != null) {
    item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem));
  }
  return item;
}

// Determine the Electron accelerator for a given Atom keystroke.
//
// keystroke - The keystroke.
//
// Returns a String containing the keystroke in a format that can be interpreted
//   by Electron to provide nice icons where available.
function acceleratorForKeystroke(keystroke) {
  if (!keystroke) {
    return null;
  }
  let modifiers = keystroke.split(/-(?=.)/);
  const key = modifiers
    .pop()
    .toUpperCase()
    .replace('+', 'Plus');

  modifiers = modifiers.map(modifier =>
    modifier
      .replace(/shift/gi, 'Shift')
      .replace(/cmd/gi, 'Command')
      .replace(/ctrl/gi, 'Ctrl')
      .replace(/alt/gi, 'Alt')
  );

  const keys = [...modifiers, key];
  return keys.join('+');
}

module.exports = {
  merge,
  unmerge,
  normalizeLabel,
  cloneMenuItem,
  acceleratorForKeystroke
};