From 23dc2d1a5b6b3db7e5daa30c71eda1add2273e34 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 19 Feb 2024 20:05:49 +0200 Subject: refactor ISS stuff into separate file --- src/services/theme_data/iss_utils.js | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/services/theme_data/iss_utils.js (limited to 'src/services/theme_data/iss_utils.js') diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js new file mode 100644 index 00000000..6568f576 --- /dev/null +++ b/src/services/theme_data/iss_utils.js @@ -0,0 +1,119 @@ +// "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 +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 selfSet = new Set() + const newCombos = previous.map(self => { + 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], []) + combos.push(flatCombos) + } + return combos.reduce((acc, x) => [...acc, ...x], []) +} + +// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { + if (!rule && !isParent) return null + const component = components[rule.component] + const { states, variants, selector, outOfTreeSelector } = component + + const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } else { + applicableVariant = variants?.normal ?? '' + } + + let realSelector + if (selector === ':root') { + realSelector = '' + } else if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] + .toSorted((a, b) => { + if (a.startsWith(':')) return 1 + if (/^[a-z]/.exec(a)) return -1 + else return 0 + }) + .join('') + + if (rule.parent) { + return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim() + } + return selectors.trim() +} + +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 +} + +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 +} + +export const normalizeCombination = rule => { + rule.variant = rule.variant ?? 'normal' + rule.state = [...new Set(['normal', ...(rule.state || [])])] +} -- cgit v1.2.3-70-g09d2 From c1568ad2ba283336378e135ce329bb4c4c1b92f2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 25 Mar 2024 18:18:48 +0200 Subject: fix massive issue in getAllPossibleCombinations --- src/services/theme_data/iss_utils.js | 16 +++++++++++++--- .../unit/specs/services/theme_data/theme_data3.spec.js | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) (limited to 'src/services/theme_data/iss_utils.js') diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js index 6568f576..2d9dd0b5 100644 --- a/src/services/theme_data/iss_utils.js +++ b/src/services/theme_data/iss_utils.js @@ -11,18 +11,28 @@ export const unroll = (item) => { } // 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 selfSet = new Set() 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], []) - combos.push(flatCombos) + const uniqueComboStrings = new Set() + const uniqueCombos = flatCombos.map(x => x.toSorted()).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], []) } @@ -31,7 +41,7 @@ export const getAllPossibleCombinations = (array) => { export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { if (!rule && !isParent) return null const component = components[rule.component] - const { states, variants, selector, outOfTreeSelector } = component + const { states = {}, variants = {}, selector, outOfTreeSelector } = component const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index 25a9dda4..37d343f9 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -11,9 +11,23 @@ import { describe.only('Theme Data 3', () => { describe('getAllPossibleCombinations', () => { - it('test simple case', () => { + it('test simple 3 values case', () => { const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) - expect(out).to.eql([[1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]) + expect(out).to.eql([ + [1], [2], [3], + [1, 2], [1, 3], [2, 3], + [1, 2, 3] + ]) + }) + + it('test simple 4 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3, 4]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], [4], + [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4], + [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4], + [1, 2, 3, 4] + ]) }) }) -- cgit v1.2.3-70-g09d2