iss_utils.js (7351B)
- import { sortBy } from 'lodash'
- // "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
- // into an array [item2, item3] for iterating
- export const unroll = (item) => {
- const out = []
- let currentParent = item
- while (currentParent) {
- out.push(currentParent)
- currentParent = currentParent.parent
- }
- return out
- }
- // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
- // Can only accept primitives. Duplicates are not supported and can cause unexpected behavior
- export const getAllPossibleCombinations = (array) => {
- const combos = [array.map(x => [x])]
- for (let comboSize = 2; comboSize <= array.length; comboSize++) {
- const previous = combos[combos.length - 1]
- const newCombos = previous.map(self => {
- const selfSet = new Set()
- self.forEach(x => selfSet.add(x))
- const nonSelf = array.filter(x => !selfSet.has(x))
- return nonSelf.map(x => [...self, x])
- })
- const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
- const uniqueComboStrings = new Set()
- const uniqueCombos = flatCombos.map(sortBy).filter(x => {
- if (uniqueComboStrings.has(x.join())) {
- return false
- } else {
- uniqueComboStrings.add(x.join())
- return true
- }
- })
- combos.push(uniqueCombos)
- }
- return combos.reduce((acc, x) => [...acc, ...x], [])
- }
- /**
- * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true)
- * selector.
- *
- * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal
- * purposes
- *
- * @param {Object} components - object containing all components definitions
- *
- * @returns {Function}
- * @param {Object} rule - rule in question to convert to CSS selector
- * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in
- * component definition and use selector
- * @param {boolean} isParent - (mostly) internal argument used when recursing
- *
- * @returns {String} CSS selector (or path)
- */
- export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
- const isParent = !!children
- if (!rule && !isParent) return null
- const component = components[rule.component]
- const { states = {}, variants = {}, outOfTreeSelector } = component
- const expand = (array = [], subArray = []) => {
- if (array.length === 0) return subArray.map(x => [x])
- if (subArray.length === 0) return array.map(x => [x])
- return array.map(a => {
- return subArray.map(b => [a, b])
- }).flat()
- }
- let componentSelectors = Array.isArray(component.selector) ? component.selector : [component.selector]
- if (ignoreOutOfTreeSelector || liteMode) componentSelectors = [componentSelectors[0]]
- componentSelectors = componentSelectors.map(selector => {
- if (selector === ':root') {
- return ''
- } else if (isParent) {
- return selector
- } else {
- if (outOfTreeSelector && !ignoreOutOfTreeSelector) return outOfTreeSelector
- return selector
- }
- })
- const applicableVariantName = (rule.variant || 'normal')
- let variantSelectors = null
- if (applicableVariantName !== 'normal') {
- variantSelectors = variants[applicableVariantName]
- } else {
- variantSelectors = variants?.normal ?? ''
- }
- variantSelectors = Array.isArray(variantSelectors) ? variantSelectors : [variantSelectors]
- if (ignoreOutOfTreeSelector || liteMode) variantSelectors = [variantSelectors[0]]
- const applicableStates = (rule.state || []).filter(x => x !== 'normal')
- // const applicableStates = (rule.state || [])
- const statesSelectors = applicableStates.map(state => {
- const selector = states[state] || ''
- let arraySelector = Array.isArray(selector) ? selector : [selector]
- if (ignoreOutOfTreeSelector || liteMode) arraySelector = [arraySelector[0]]
- arraySelector
- .sort((a) => {
- if (a.startsWith(':')) return 1
- if (/^[a-z]/.exec(a)) return -1
- else return 0
- })
- .join('')
- return arraySelector
- })
- const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
- return expand(acc, s).map(st => st.join(''))
- }, [])
- const componentVariant = expand(componentSelectors, variantSelectors).map(cv => cv.join(''))
- const componentVariantStates = expand(componentVariant, statesSelectorsFlat).map(cvs => cvs.join(''))
- const selectors = expand(componentVariantStates, children).map(cvsc => cvsc.join(' '))
- /*
- */
- if (rule.parent) {
- return genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, liteMode, selectors)
- }
- return selectors.join(', ').trim()
- }
- /**
- * Check if combination matches
- *
- * @param {Object} criteria - criteria to match against
- * @param {Object} subject - rule/combination to check match
- * @param {boolean} strict - strict checking:
- * By default every variant and state inherits from "normal" state/variant
- * so when checking if combination matches, it WILL match against "normal"
- * state/variant. In strict mode inheritance is ignored an "normal" does
- * not match
- */
- export const combinationsMatch = (criteria, subject, strict) => {
- if (criteria.component !== subject.component) return false
- // All variants inherit from normal
- if (subject.variant !== 'normal' || strict) {
- if (criteria.variant !== subject.variant) return false
- }
- // Subject states > 1 essentially means state is "normal" and therefore matches
- if (subject.state.length > 1 || strict) {
- const subjectStatesSet = new Set(subject.state)
- const criteriaStatesSet = new Set(criteria.state)
- const setsAreEqual =
- [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
- [...subjectStatesSet].every(state => criteriaStatesSet.has(state))
- if (!setsAreEqual) return false
- }
- return true
- }
- /**
- * Search for rule that matches `criteria` in set of rules
- * meant to be used in a ruleset.filter() function
- *
- * @param {Object} criteria - criteria to search for
- * @param {boolean} strict - whether search strictly or not (see combinationsMatch)
- *
- * @return function that returns true/false if subject matches
- */
- export const findRules = (criteria, strict) => subject => {
- // If we searching for "general" rules - ignore "specific" ones
- if (criteria.parent === null && !!subject.parent) return false
- if (!combinationsMatch(criteria, subject, strict)) return false
- if (criteria.parent !== undefined && criteria.parent !== null) {
- if (!subject.parent && !strict) return true
- const pathCriteria = unroll(criteria)
- const pathSubject = unroll(subject)
- if (pathCriteria.length < pathSubject.length) return false
- // Search: .a .b .c
- // Matches: .a .b .c; .b .c; .c; .z .a .b .c
- // Does not match .a .b .c .d, .a .b .e
- for (let i = 0; i < pathCriteria.length; i++) {
- const criteriaParent = pathCriteria[i]
- const subjectParent = pathSubject[i]
- if (!subjectParent) return true
- if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false
- }
- }
- return true
- }
- // Pre-fills 'normal' state/variant if missing
- export const normalizeCombination = rule => {
- rule.variant = rule.variant ?? 'normal'
- rule.state = [...new Set(['normal', ...(rule.state || [])])]
- }