// UTILS function splitArray(arr, predicate) { let lastArr = []; const multiArr = [lastArr]; arr.forEach(item => { if (predicate(item)) { if (lastArr.length > 0) { lastArr = []; multiArr.push(lastArr); } } else { lastArr.push(item); } }); return multiArr; } function joinArrays(arrays, joiner) { const joinedArr = []; arrays.forEach((arr, i) => { if (i > 0 && arr.length > 0) { joinedArr.push(joiner); } joinedArr.push(...arr); }); return joinedArr; } const pushOntoMultiMap = (map, key, value) => { if (!map.has(key)) { map.set(key, []); } map.get(key).push(value); }; function indexOfGroupContainingCommand(groups, command, ignoreGroup) { return groups.findIndex( candiateGroup => candiateGroup !== ignoreGroup && candiateGroup.some(candidateItem => candidateItem.command === command) ); } // Sort nodes topologically using a depth-first approach. Encountered cycles // are broken. function sortTopologically(originalOrder, edgesById) { const sorted = []; const marked = new Set(); function visit(id) { if (marked.has(id)) { // Either this node has already been placed, or we have encountered a // cycle and need to exit. return; } marked.add(id); const edges = edgesById.get(id); if (edges != null) { edges.forEach(visit); } sorted.push(id); } originalOrder.forEach(visit); return sorted; } function attemptToMergeAGroup(groups) { for (let i = 0; i < groups.length; i++) { const group = groups[i]; for (const item of group) { const toCommands = [...(item.before || []), ...(item.after || [])]; for (const command of toCommands) { const index = indexOfGroupContainingCommand(groups, command, group); if (index === -1) { // No valid edge for this command continue; } const mergeTarget = groups[index]; // Merge with group containing `command` mergeTarget.push(...group); groups.splice(i, 1); return true; } } } return false; } // Merge groups based on before/after positions // Mutates both the array of groups, and the individual group arrays. function mergeGroups(groups) { let mergedAGroup = true; while (mergedAGroup) { mergedAGroup = attemptToMergeAGroup(groups); } return groups; } function sortItemsInGroup(group) { const originalOrder = group.map((node, i) => i); const edges = new Map(); const commandToIndex = new Map(group.map((item, i) => [item.command, i])); group.forEach((item, i) => { if (item.before) { item.before.forEach(toCommand => { const to = commandToIndex.get(toCommand); if (to != null) { pushOntoMultiMap(edges, to, i); } }); } if (item.after) { item.after.forEach(toCommand => { const to = commandToIndex.get(toCommand); if (to != null) { pushOntoMultiMap(edges, i, to); } }); } }); const sortedNodes = sortTopologically(originalOrder, edges); return sortedNodes.map(i => group[i]); } function findEdgesInGroup(groups, i, edges) { const group = groups[i]; for (const item of group) { if (item.beforeGroupContaining) { for (const command of item.beforeGroupContaining) { const to = indexOfGroupContainingCommand(groups, command, group); if (to !== -1) { pushOntoMultiMap(edges, to, i); return; } } } if (item.afterGroupContaining) { for (const command of item.afterGroupContaining) { const to = indexOfGroupContainingCommand(groups, command, group); if (to !== -1) { pushOntoMultiMap(edges, i, to); return; } } } } } function sortGroups(groups) { const originalOrder = groups.map((item, i) => i); const edges = new Map(); for (let i = 0; i < groups.length; i++) { findEdgesInGroup(groups, i, edges); } const sortedGroupIndexes = sortTopologically(originalOrder, edges); return sortedGroupIndexes.map(i => groups[i]); } function isSeparator(item) { return item.type === 'separator'; } function sortMenuItems(menuItems) { // Split the items into their implicit groups based upon separators. const groups = splitArray(menuItems, isSeparator); // Merge groups that contain before/after references to eachother. const mergedGroups = mergeGroups(groups); // Sort each individual group internally. const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup); // Sort the groups based upon their beforeGroupContaining/afterGroupContaining // references. const sortedGroups = sortGroups(mergedGroupsWithSortedItems); // Join the groups back return joinArrays(sortedGroups, { type: 'separator' }); } module.exports = { sortMenuItems };