import type * as Style from './index.js' import * as Set from './methods/set.js' import { Rule } from './rule.js' import { Context } from './context.js' import { getAspects, TypeDefinitions, isTypeElementAspect } from './definition.js' export * from './methods/set.js' import * as ClassList from './methods/class-list.js' import * as Conventions from './methods/conventions.js' /** * ## Style.Set -- manipulating sets of styles * * Style.Set is array of style rules. Methods in this module provide different ways to look the styles up. It's simple * things like finding styles by id, slug and the like, but also the domain-specific lookups like determining which * style is currently active for a context element. */ /** * Run fixers for all styles in a set. * * @param {Style.Rule[]} rules - The array of style rules. * @returns {Style.Rule[]} The array of style rules after running fixers. * @example * * const fixedRules = fix(originalRules) * const refixedRules = fix(fixedRules) // refixedRules should be equal to fixedRules. * */ export function fix(rules: Style.Rule[]): Style.Rule[] { return rules.map((s) => Rule.fix(s, rules)) } /** * List all choices of specific aspect type for given element. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.Rule.Element} style - The element style. * @param {Style.Type.ElementAspect} type - The element aspect type. * @returns {Style.Rule[]} The array of aspect choices for the given element. * @example * * const decorationAspectChoices = getElementAspectChoices(rules, elementStyle, 'decoration') * const typographyAspectChoices = getElementAspectChoices(rules, anotherElementStyle, 'typography') * */ export function getElementAspectChoices< S extends Style.Rule.Element, T extends Style.Type.ElementAspect> >(rules: Style.Rule[], style: S, type: T): Style.Rule[] { const aspect = TypeDefinitions[type] const ids = style?.props?.[aspect.property as keyof typeof style.props] if (ids == null || ids.length == 0) return Set.filterByType(rules, type).sort((a, b) => Number(a.details.isInternal) - Number(b.details.isInternal)) return ids.map((id) => Set.findById(rules, id)) } /** * For each possible aspect, list all choices. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.Rule.Element} element - The element style. * @returns {Object} The aspects by type. * @example * * const headerAspectsByType = getElementAspectChoicesByType(rules, headerElementStyle) * const paragraphAspectsByType = getElementAspectChoicesByType(rules, paragraphElementStyle) * */ export function getElementAspectChoicesByType< S extends Style.Rule.Element, T extends Style.Type.Element = Style.typeOf >(rules: Style.Rule[], element: S): Style.AspectsByType { return getAspects(element.type).reduce((result, aspect) => { return Object.assign(result, { [aspect]: getElementAspectChoices(rules, element, aspect) }) }, {} as Style.AspectsByType) } /** * Get element's assigned aspect. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.ContextOrClassList} context - The context or class list. * @param {Style.Type.Aspect} type - The aspect type. * @returns {Style.Rule} The assigned aspect for the given element. * @example * * const assignedDecorationAspect = getContextAssignedAspect(rules, context, 'decoration') * const assignedTypographyAspect = getContextAssignedAspect(rules, context, 'typography') * */ export function getContextAssignedAspect( rules: Style.Rule[], context: Style.ContextOrClassList, type: T ): Style.Rule { const classList = Context.getClassList(context) return Set.filterByType(getContextAssigned(rules, classList), type)[0] } /** * Get context default aspect as defined by element style. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.ContextOrClassList} context - The context or class list. * @param {Style.Type.Aspect} type - The aspect type. * @returns {Style.Rule} The default aspect for the given context. * @example * * const defaultDecorationAspect = getContextDefaultAspect(rules, context, 'decoration') * const defaultTypographyAspect = getContextDefaultAspect(rules, context, 'typography') * */ export function getContextDefaultAspect( rules: Style.Rule[], context: Style.ContextOrClassList, type: T ): Style.Rule { const classList = Context.getClassList(context) const element = getContextElement(rules, classList) if (!element || !isTypeElementAspect(type) || !getAspects(element.type)?.includes(type)) return return Set.getAspectDefault(getElementAspectChoices(rules, element, type)) } /** * Get assigned context aspect in set of custom styles. * * @example * * const customDecorationAspect = getContextCustomAspect(customRules, context, 'decoration') * const customTypographyAspect = getContextCustomAspect(customRules, context, 'typography') * */ export function getContextCustomAspect( customRules: Style.Rule[], context: Style.Context, type: T ): Style.Rule { const instanceId = Context.getInstanceId(context) const contextCustomStyles = Set.filterByType(Set.filterByInstanceId(customRules, instanceId), type) return getContextAssigned(contextCustomStyles, context)[0] } /** * List all possible element rules that can be applicable to given context. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.ContextOrClassList} context - The context or class list. * @returns {Style.Rule.Element[]} The array of possible element rules for the given context. * @example * * const headerElementChoices = getContextElementChoices(rules, headerContext) * const paragraphElementChoices = getContextElementChoices(rules, paragraphContext) * */ export function getContextElementChoices(rules: Style.Rule[], context: Style.ContextOrClassList): Style.Rule.Element[] { const classList = Context.getClassList(context) return getDefinitionElementChoices(rules, getContextDefinition(rules, classList)) } /** * List all possible element rules that can be applicable to given element definition. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.ElementDefinition} definition - Element definition. * @returns {Style.Rule.Element[]} The array of possible element rules for the given context. * @example * * const headerElementChoices = getContextElementChoices(rules, headerDefinition) * const paragraphElementChoices = getContextElementChoices(rules, paragraphDefinition) * */ export function getDefinitionElementChoices( rules: Style.Rule[], definition: Style.ElementDefinition ): Style.Rule.Element[] { if (!definition) return [] const collection = Set.filterCollectionsForType(rules, definition.type).find((c) => c.details.id == definition.id) if (!collection) return [] return Set.filterByType(rules, definition.type).filter((style) => style.details.collectionId == collection.details.id) } /** * Get all available aspect choices as defined by context's element rule. * * @param {Style.Rule[]} rules - The array of style rules. * @param {Style.ContextOrClassList} context - The context or class list. * @param {Style.Type.ElementAspect} type - The element aspect type. * @returns {Style.Rule[]} The array of available aspect choices for the given context's element rule. * @example * * const decorationChoices = getContextAspectChoices(rules, context, 'decoration') * const typographyChoices = getContextAspectChoices(rules, context, 'typography') * */ export function getContextAspectChoices( rules: Style.Rule[], context: Style.ContextOrClassList, type: T ): Style.Rule[] { return getElementAspectChoices(rules, getContextElement(rules, context), type) } /** Get assigned element rule for given context. Returns null if it's generic element */ export function getContextElement(rules: Style.Rule[], context: Style.ContextOrClassList): Style.Rule.Element { const classList = Context.getClassList(context) const choices = getContextElementChoices(rules, classList) return getContextAssigned(choices, classList)[0] } /** Return element definition for given context */ export function getContextDefinition(rules: Style.Rule[], context: Style.ContextOrClassList, followDisguise = true) { const classList = Context.getClassList(context) const definitions = Set.getElementDefinitions(rules) const className = ClassList.getElementClassName(classList, definitions) if (!className) return null const definition = definitions.find((definition) => Conventions.matchClassName(className, definition.className)) if (followDisguise && definition?.disguise) { return definitions.find((d) => d.type == definition.type && d.name == definition.disguise) } return definition } /** List styles that are referenced explicitly in class list */ export function getContextAssigned( rules: S[], context: Style.ContextOrClassList ): Extract[] { const classList = Context.getClassList(context) return rules.filter((style) => { return classList.includes(Rule.getClassName(style)) }) as Extract[] } /** * Ultimate aspect getter that returns aspect for given context wether it's: * * - Custom rule assigned to element * - Assigned to element but not custom * - Default element aspect * - Aspect as defined by assigned combo * - Aspect as defined by inherited theme combo * - Aspect as defined by default theme combo */ export function getContextAspect( rules: Style.Rule[], context: Style.Context, type: T, themeContext: Style.ContextOrClassList, customRules: Style.Rule[] ) { return ( getContextElementAspect(rules, context, type, customRules) || (isTypeElementAspect(type) && getContextComboAspect(rules, context, type, themeContext)) || null ) } /** Find aspect style for given context (custom, assigned or default) */ export function getContextElementAspect( rules: Style.Rule[], context: Style.Context, type: T, customRules: Style.Rule[] ): Style.Rule { return ( getContextCustomAspect(customRules, context, type) || getContextAssignedAspect(rules, context, type) || getContextDefaultAspect(rules, context, type) ) } export function getContextAspects( rules: Style.Rule[], context: Style.Context, themeContext: Style.ContextOrClassList, customRules: Style.Rule[] ) { return (['fill', 'decoration', 'spacing', 'typography', 'palette'] as const).reduce((object, type) => { return { ...object, [type]: getContextAspect(rules, context, type, themeContext, customRules) } }, {}) as { [key in T]: Style.Rule } } /** Get combo for given context (assigned or inherited from theme) */ export function getContextCombo( rules: Style.Rule[], context: Style.ContextOrClassList, themeContext: Style.ContextOrClassList ) { return ( getContextAssignedCombo(rules, context) || getContextThemeCombo(rules, context, themeContext) || getContextThemeDefaultCombo(rules, context, themeContext) ) } /** Get theme for given context */ export function getContextTheme(rules: Style.Rule[], context: Style.ContextOrClassList) { return Set.findBySlug(rules, 'theme', ClassList.getThemeSlug(Context.getClassList(context))) } /** Get combo explicitly assigned to given context */ export function getContextAssignedCombo(rules: Style.Rule[], context: Style.ContextOrClassList) { return Set.findCombo(rules, ClassList.getComboId(Context.getClassList(context))) } /** Get theme combo matching given context */ export function getContextThemeCombo( rules: Style.Rule[], context: Style.ContextOrClassList, themeContext: Style.ContextOrClassList ) { const classList = Context.getClassList(context) const comboIds = ClassList.getThemeComboIds(Context.getClassList(themeContext)) const comboId = comboIds.find((comboId) => ClassList.matchesComboId(classList, comboId, rules)) if (comboId) return Set.findCombo(rules, comboId) } /** Get theme combo that is chosen as default that will match given context element */ export function getContextThemeDefaultCombo( rules: Style.Rule[], context: Style.ContextOrClassList, themeContext: Style.ContextOrClassList ) { const theme = getContextTheme(rules, themeContext) if (!theme) return const elements = getContextElementChoices(rules, context) if (!elements) return const definition = getContextDefinition(rules, context) return theme.props[`${definition.type}s`].find( (combo) => combo.isDefault && elements.find((e) => e.details.id == combo.refId) ) } /** Get aspect specified by combo for given context */ export function getContextComboAspect( rules: Style.Rule[], context: Style.ContextOrClassList, type: T, themeContext: Style.ContextOrClassList ): Style.Rule { const combo = getContextCombo(rules, context, themeContext) if (!combo) return null return Set.findById(rules, combo[`${type}Id` as const]) } /** Given a set of element choices, return all matching combos in a theme */ export function getElementsThemeCombos(elementChoices: Style.Rule[], theme: Style.Rule<'theme'>) { return Object.values(theme.props) .map((comboIds) => { return comboIds.filter((item) => elementChoices.find((s) => s?.details.id === item.refId)) }) .flat() } /** Analyze the level of customization for given element rule */ export function getElementCustomizationType>( effectiveRules: Style.Rule[], element: S, assignedRules: Style.Rule.ElementAspect[] ) { const aspectOptions = getElementAspectChoicesByType(effectiveRules, element) return assignedRules.find((style) => style.details.instanceId) ? 'oneOff' : assignedRules.every((style) => { const defaultStyle = Set.getAspectDefault(aspectOptions[style.type]) return style.details.id === defaultStyle?.details.id }) ? 'default' : assignedRules.some((style) => !aspectOptions[style.type]?.some((s) => style.details.id === s.details.id)) ? 'nonAllowed' : 'allowed' } export function getDefinitionCustomizationType>( rules: Style.Rule[], definition: Style.ElementDefinition, assignedRules: Style.Rule.ElementAspect[] ) { const aspectOptions = getAspects(definition.type).reduce((result, aspect) => { return Object.assign(result, { [aspect]: Set.filterByType(rules, aspect).sort( (a, b) => Number(a.details.isInternal) - Number(b.details.isInternal) ) }) }, {} as Style.AspectsByType) return assignedRules.find((style) => style.details.instanceId) ? 'oneOff' : assignedRules.every((style) => { const defaultStyle = Set.getAspectDefault(aspectOptions[style.type]) return style.details.id === defaultStyle?.details.id }) ? 'default' : assignedRules.some((style) => !aspectOptions[style.type]?.some((s) => style.details.id === s.details.id)) ? 'nonAllowed' : 'allowed' } /* export function forClassList( styles: Style.Rule[], customRules: Style.Rule[], theme: Style.Rule<'theme'>, classList: string[] ) { const effectiveStyles = Set.join(styles, customRules) const assignedStyles = Set.getContextAssigned(effectiveStyles, classList) const element = getContextElement(styles, classList) const definition = Set.getElementDefinitionFromClassList(styles, classList) const allElements = Set.getElementChoicesFromClassList(styles, classList) const allCombos = getElementsThemeCombos(allElements, theme) const usedCombo = !element && ClassList.findCombo(classList, allCombos) return { customRules, effectiveStyles, classList, assignedStyles, theme, element, definition, allElements, allCombos, usedCombo } } */