function splitArray (arr, predicate) { const result = arr.reduce((multi, item) => { const current = multi[multi.length - 1] if (predicate(item)) { if (current.length > 0) multi.push([]) } else { current.push(item) } return multi }, [[]]) if (result[result.length - 1].length === 0) { return result.slice(0, result.length - 1) } return result } function joinArrays (arrays, joiner) { return arrays.reduce((joined, arr, i) => { if (i > 0 && arr.length) { joined.push(joiner) } return joined.concat(arr) }, []) } function pushOntoMultiMap (map, key, value) { if (!map.has(key)) { map.set(key, []) } map.get(key).push(value) } function indexOfGroupContainingID (groups, id, ignoreGroup) { return groups.findIndex( candidateGroup => candidateGroup !== ignoreGroup && candidateGroup.some( candidateItem => candidateItem.id === id ) ) } // Sort nodes topologically using a depth-first approach. Encountered cycles // are broken. function sortTopologically (originalOrder, edgesById) { const sorted = [] const marked = new Set() const visit = (mark) => { if (marked.has(mark)) return marked.add(mark) const edges = edgesById.get(mark) if (edges != null) { edges.forEach(visit) } sorted.push(mark) } 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 toIDs = [...(item.before || []), ...(item.after || [])] for (const id of toIDs) { const index = indexOfGroupContainingID(groups, id, group) if (index === -1) continue const mergeTarget = groups[index] mergeTarget.push(...group) groups.splice(i, 1) return true } } } return false } function mergeGroups (groups) { let merged = true while (merged) { merged = attemptToMergeAGroup(groups) } return groups } function sortItemsInGroup (group) { const originalOrder = group.map((node, i) => i) const edges = new Map() const idToIndex = new Map(group.map((item, i) => [item.id, i])) group.forEach((item, i) => { if (item.before) { item.before.forEach(toID => { const to = idToIndex.get(toID) if (to != null) { pushOntoMultiMap(edges, to, i) } }) } if (item.after) { item.after.forEach(toID => { const to = idToIndex.get(toID) 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 id of item.beforeGroupContaining) { const to = indexOfGroupContainingID(groups, id, group) if (to !== -1) { pushOntoMultiMap(edges, to, i) return } } } if (item.afterGroupContaining) { for (const id of item.afterGroupContaining) { const to = indexOfGroupContainingID(groups, id, 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 sortMenuItems (menuItems) { const isSeparator = (item) => item.type === 'separator' // Split the items into their implicit groups based upon separators. const groups = splitArray(menuItems, isSeparator) const mergedGroups = mergeGroups(groups) const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup) const sortedGroups = sortGroups(mergedGroupsWithSortedItems) const joined = joinArrays(sortedGroups, { type: 'separator' }) return joined } module.exports = {sortMenuItems}