logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

iss_utils.js (7351B)


  1. import { sortBy } from 'lodash'
  2. // "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
  3. // into an array [item2, item3] for iterating
  4. export const unroll = (item) => {
  5. const out = []
  6. let currentParent = item
  7. while (currentParent) {
  8. out.push(currentParent)
  9. currentParent = currentParent.parent
  10. }
  11. return out
  12. }
  13. // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
  14. // Can only accept primitives. Duplicates are not supported and can cause unexpected behavior
  15. export const getAllPossibleCombinations = (array) => {
  16. const combos = [array.map(x => [x])]
  17. for (let comboSize = 2; comboSize <= array.length; comboSize++) {
  18. const previous = combos[combos.length - 1]
  19. const newCombos = previous.map(self => {
  20. const selfSet = new Set()
  21. self.forEach(x => selfSet.add(x))
  22. const nonSelf = array.filter(x => !selfSet.has(x))
  23. return nonSelf.map(x => [...self, x])
  24. })
  25. const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
  26. const uniqueComboStrings = new Set()
  27. const uniqueCombos = flatCombos.map(sortBy).filter(x => {
  28. if (uniqueComboStrings.has(x.join())) {
  29. return false
  30. } else {
  31. uniqueComboStrings.add(x.join())
  32. return true
  33. }
  34. })
  35. combos.push(uniqueCombos)
  36. }
  37. return combos.reduce((acc, x) => [...acc, ...x], [])
  38. }
  39. /**
  40. * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true)
  41. * selector.
  42. *
  43. * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal
  44. * purposes
  45. *
  46. * @param {Object} components - object containing all components definitions
  47. *
  48. * @returns {Function}
  49. * @param {Object} rule - rule in question to convert to CSS selector
  50. * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in
  51. * component definition and use selector
  52. * @param {boolean} isParent - (mostly) internal argument used when recursing
  53. *
  54. * @returns {String} CSS selector (or path)
  55. */
  56. export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
  57. const isParent = !!children
  58. if (!rule && !isParent) return null
  59. const component = components[rule.component]
  60. const { states = {}, variants = {}, outOfTreeSelector } = component
  61. const expand = (array = [], subArray = []) => {
  62. if (array.length === 0) return subArray.map(x => [x])
  63. if (subArray.length === 0) return array.map(x => [x])
  64. return array.map(a => {
  65. return subArray.map(b => [a, b])
  66. }).flat()
  67. }
  68. let componentSelectors = Array.isArray(component.selector) ? component.selector : [component.selector]
  69. if (ignoreOutOfTreeSelector || liteMode) componentSelectors = [componentSelectors[0]]
  70. componentSelectors = componentSelectors.map(selector => {
  71. if (selector === ':root') {
  72. return ''
  73. } else if (isParent) {
  74. return selector
  75. } else {
  76. if (outOfTreeSelector && !ignoreOutOfTreeSelector) return outOfTreeSelector
  77. return selector
  78. }
  79. })
  80. const applicableVariantName = (rule.variant || 'normal')
  81. let variantSelectors = null
  82. if (applicableVariantName !== 'normal') {
  83. variantSelectors = variants[applicableVariantName]
  84. } else {
  85. variantSelectors = variants?.normal ?? ''
  86. }
  87. variantSelectors = Array.isArray(variantSelectors) ? variantSelectors : [variantSelectors]
  88. if (ignoreOutOfTreeSelector || liteMode) variantSelectors = [variantSelectors[0]]
  89. const applicableStates = (rule.state || []).filter(x => x !== 'normal')
  90. // const applicableStates = (rule.state || [])
  91. const statesSelectors = applicableStates.map(state => {
  92. const selector = states[state] || ''
  93. let arraySelector = Array.isArray(selector) ? selector : [selector]
  94. if (ignoreOutOfTreeSelector || liteMode) arraySelector = [arraySelector[0]]
  95. arraySelector
  96. .sort((a) => {
  97. if (a.startsWith(':')) return 1
  98. if (/^[a-z]/.exec(a)) return -1
  99. else return 0
  100. })
  101. .join('')
  102. return arraySelector
  103. })
  104. const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
  105. return expand(acc, s).map(st => st.join(''))
  106. }, [])
  107. const componentVariant = expand(componentSelectors, variantSelectors).map(cv => cv.join(''))
  108. const componentVariantStates = expand(componentVariant, statesSelectorsFlat).map(cvs => cvs.join(''))
  109. const selectors = expand(componentVariantStates, children).map(cvsc => cvsc.join(' '))
  110. /*
  111. */
  112. if (rule.parent) {
  113. return genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, liteMode, selectors)
  114. }
  115. return selectors.join(', ').trim()
  116. }
  117. /**
  118. * Check if combination matches
  119. *
  120. * @param {Object} criteria - criteria to match against
  121. * @param {Object} subject - rule/combination to check match
  122. * @param {boolean} strict - strict checking:
  123. * By default every variant and state inherits from "normal" state/variant
  124. * so when checking if combination matches, it WILL match against "normal"
  125. * state/variant. In strict mode inheritance is ignored an "normal" does
  126. * not match
  127. */
  128. export const combinationsMatch = (criteria, subject, strict) => {
  129. if (criteria.component !== subject.component) return false
  130. // All variants inherit from normal
  131. if (subject.variant !== 'normal' || strict) {
  132. if (criteria.variant !== subject.variant) return false
  133. }
  134. // Subject states > 1 essentially means state is "normal" and therefore matches
  135. if (subject.state.length > 1 || strict) {
  136. const subjectStatesSet = new Set(subject.state)
  137. const criteriaStatesSet = new Set(criteria.state)
  138. const setsAreEqual =
  139. [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
  140. [...subjectStatesSet].every(state => criteriaStatesSet.has(state))
  141. if (!setsAreEqual) return false
  142. }
  143. return true
  144. }
  145. /**
  146. * Search for rule that matches `criteria` in set of rules
  147. * meant to be used in a ruleset.filter() function
  148. *
  149. * @param {Object} criteria - criteria to search for
  150. * @param {boolean} strict - whether search strictly or not (see combinationsMatch)
  151. *
  152. * @return function that returns true/false if subject matches
  153. */
  154. export const findRules = (criteria, strict) => subject => {
  155. // If we searching for "general" rules - ignore "specific" ones
  156. if (criteria.parent === null && !!subject.parent) return false
  157. if (!combinationsMatch(criteria, subject, strict)) return false
  158. if (criteria.parent !== undefined && criteria.parent !== null) {
  159. if (!subject.parent && !strict) return true
  160. const pathCriteria = unroll(criteria)
  161. const pathSubject = unroll(subject)
  162. if (pathCriteria.length < pathSubject.length) return false
  163. // Search: .a .b .c
  164. // Matches: .a .b .c; .b .c; .c; .z .a .b .c
  165. // Does not match .a .b .c .d, .a .b .e
  166. for (let i = 0; i < pathCriteria.length; i++) {
  167. const criteriaParent = pathCriteria[i]
  168. const subjectParent = pathSubject[i]
  169. if (!subjectParent) return true
  170. if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false
  171. }
  172. }
  173. return true
  174. }
  175. // Pre-fills 'normal' state/variant if missing
  176. export const normalizeCombination = rule => {
  177. rule.variant = rule.variant ?? 'normal'
  178. rule.state = [...new Set(['normal', ...(rule.state || [])])]
  179. }