@cssfn/cssfn
Advanced tools
Comparing version 1.0.8 to 1.0.9
@@ -1,11 +0,26 @@ | ||
import { JssStyle, JssValue, Classes, Styles, StyleSheet } from 'jss'; | ||
import { ExtendableStyle } from '@cssfn/jss-plugin-extend'; | ||
import type { OptionalOrFalse, SingleOrArray, SingleOrDeepArray, ProductOrFactoryOrDeepArray, ProductOrFactory, Dictionary, ValueOf, DictionaryOf } from '@cssfn/types'; | ||
import { Classes, Styles, StyleSheet } from 'jss'; | ||
import type { OptionalOrFalse, SingleOrDeepArray, ProductOrFactoryOrDeepArray, ProductOrFactory, Dictionary, ValueOf, DictionaryOf } from '@cssfn/types'; | ||
import type { Prop, PropEx, Cust } from '@cssfn/css-types'; | ||
import { Combinator, SelectorList as SelectorModelList } from '@cssfn/css-selector'; | ||
import { Properties as CssProperties } from 'csstype'; | ||
import { pascalCase } from 'pascal-case'; | ||
import { camelCase } from 'camel-case'; | ||
export type { JssStyle, JssValue, Classes, Styles, StyleSheet }; | ||
export type { Classes, Styles, StyleSheet }; | ||
export type { Prop, PropEx, Cust }; | ||
export type { Dictionary, ValueOf, DictionaryOf }; | ||
export declare type Style = (ExtendableStyle & {}); | ||
export declare type KnownCssPropName = keyof CssProperties<string | number>; | ||
export declare type KnownCssPropValue<PropName extends KnownCssPropName> = Exclude<CssProperties<string | number>[PropName], (undefined | null)>; | ||
export declare type KnownCssProps = { | ||
[PropName in keyof CssProperties<string | number>]?: (KnownCssPropValue<PropName> | [[KnownCssPropValue<PropName>], '!important'] | CssValue); | ||
}; | ||
export declare type BasicCssValue = (string & {}) | (number & {}) | PropEx.Keyframes; | ||
export declare type CssValue = undefined | null | BasicCssValue | BasicCssValue[] | (BasicCssValue | BasicCssValue[] | '!important')[]; | ||
export declare type CustomCssProps = { | ||
[PropName: Exclude<string, KnownCssPropName>]: CssValue; | ||
}; | ||
export declare type CssProps = KnownCssProps & CustomCssProps; | ||
export declare type Rule = { | ||
[PropName: symbol]: StyleCollection; | ||
}; | ||
export declare type Style = CssProps & Rule; | ||
export declare type StyleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Style>>; | ||
@@ -19,17 +34,5 @@ export declare type ClassName = string; | ||
export declare type OptionalString = OptionalOrFalse<string>; | ||
export declare type UniversalSelector = ('*' & {}); | ||
export declare type RealElementSelector = (string & {}); | ||
export declare type PseudoElementSelector = (`::${string}` & {}); | ||
export declare type ElementSelector = RealElementSelector | PseudoElementSelector; | ||
export declare type ClassSelector = (Class & {}); | ||
export declare type IdSelector = (`#${string}` & {}); | ||
export declare type SingleSelector = UniversalSelector | ElementSelector | ClassSelector | IdSelector; | ||
export declare type Selector = SingleSelector | (`${SingleSelector}${SingleSelector}` & {}) | (`${SingleSelector}${SingleSelector}${SingleSelector}` & {}) | (`${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}` & {}) | (`${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}` & {}); | ||
export declare type Selector = (string & {}); | ||
export declare type SelectorCollection = SingleOrDeepArray<OptionalOrFalse<Selector>>; | ||
export declare type NestedSelector = '&' | (`&${Selector}` & {}) | (`${Selector}&` & {}); | ||
export declare type RuleEntry = readonly [SelectorCollection, StyleCollection]; | ||
export declare type RuleEntrySource = ProductOrFactory<OptionalOrFalse<RuleEntry>>; | ||
export declare type RuleList = RuleEntrySource[]; | ||
export declare type RuleCollection = SingleOrArray<RuleEntrySource | RuleList>; | ||
export declare type PropList = Dictionary<JssValue>; | ||
export declare type RuleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Rule>>; | ||
export declare const createJssSheet: <TClassName extends string = string>(styles: ProductOrFactory<Styles<TClassName, unknown, undefined>>, sheetId?: string | undefined) => StyleSheet<TClassName>; | ||
@@ -39,7 +42,2 @@ export declare const createSheet: <TClassName extends string = string>(classes: ProductOrFactory<ClassList<TClassName>>, sheetId?: string | undefined) => StyleSheet<TClassName>; | ||
/** | ||
* Defines the (sub) component's composition. | ||
* @returns A `StyleCollection` represents the (sub) component's composition. | ||
*/ | ||
export declare const composition: (styles: StyleCollection[]) => StyleCollection; | ||
/** | ||
* Merges the (sub) component's composition to single `Style`. | ||
@@ -51,2 +49,9 @@ * @returns A `Style` represents the merged (sub) component's composition | ||
export declare const mergeStyles: (styles: StyleCollection) => Style | null; | ||
export interface SelectorOptions { | ||
groupSelectors?: boolean; | ||
specificityWeight?: number | null; | ||
minSpecificityWeight?: number | null; | ||
maxSpecificityWeight?: number | null; | ||
} | ||
export declare const mergeSelectors: (selectorList: SelectorModelList, options?: SelectorOptions) => SelectorModelList; | ||
/** | ||
@@ -56,3 +61,3 @@ * Defines the additional component's composition. | ||
*/ | ||
export declare const compositionOf: <TClassName extends string = string>(className: TClassName, styles: StyleCollection[]) => ClassEntry<TClassName>; | ||
export declare const compositionOf: <TClassName extends string = string>(className: TClassName, ...styles: StyleCollection[]) => ClassEntry<TClassName>; | ||
/** | ||
@@ -62,3 +67,3 @@ * Defines the main component's composition. | ||
*/ | ||
export declare const mainComposition: (styles: StyleCollection[]) => ClassEntry<"main">; | ||
export declare const mainComposition: (...styles: StyleCollection[]) => ClassEntry<"main">; | ||
/** | ||
@@ -68,69 +73,80 @@ * Defines the global style applied to a whole document. | ||
*/ | ||
export declare const globalDef: (ruleCollection: RuleCollection) => ClassEntry<"">; | ||
export declare const imports: (styles: StyleCollection[]) => StyleCollection; | ||
export declare const globalDef: (...rules: RuleCollection[]) => ClassEntry<"">; | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines the (sub) component's composition. | ||
* @returns A `Rule` represents the (sub) component's composition. | ||
*/ | ||
export declare const composition: (...styles: StyleCollection[]) => Rule; | ||
/** | ||
* Defines component's style. | ||
* @returns A `Rule` represents the component's style. | ||
*/ | ||
export declare const style: (style: Style) => Rule; | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines component's layout. | ||
* @returns A `Style` represents the component's layout. | ||
* @returns A `Rule` represents the component's layout. | ||
*/ | ||
export declare const layout: (style: Style) => Style; | ||
export declare const layout: (style: Style) => Rule; | ||
/** | ||
* Defines component's variable(s). | ||
* @returns A `Style` represents the component's variable(s). | ||
* @returns A `Rule` represents the component's variable(s). | ||
*/ | ||
export declare const vars: (items: { | ||
[name: string]: JssValue; | ||
}) => Style; | ||
export interface CombinatorOptions { | ||
groupSelectors?: boolean; | ||
} | ||
export declare const combinators: (combinator: string, selectors: SelectorCollection, styles: StyleCollection, options?: CombinatorOptions) => PropList; | ||
export declare const descendants: (selectors: SelectorCollection, styles: StyleCollection, options?: CombinatorOptions) => PropList; | ||
export declare const children: (selectors: SelectorCollection, styles: StyleCollection, options?: CombinatorOptions) => PropList; | ||
export declare const siblings: (selectors: SelectorCollection, styles: StyleCollection, options?: CombinatorOptions) => PropList; | ||
export declare const nextSiblings: (selectors: SelectorCollection, styles: StyleCollection, options?: CombinatorOptions) => PropList; | ||
export interface RuleOptions { | ||
minSpecificityWeight?: number; | ||
} | ||
export declare const rules: (ruleCollection: RuleCollection, options?: RuleOptions) => StyleCollection; | ||
[key: `--${string}`]: CssValue; | ||
}) => Rule; | ||
export declare const imports: (...styles: StyleCollection[]) => Rule; | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `Rule` represents the component's rule. | ||
*/ | ||
export declare const rule: (rules: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const rules: (rules: RuleCollection, options?: SelectorOptions) => Rule; | ||
/** | ||
* Defines component's variants. | ||
* @returns A `StyleCollection` represents the component's variants. | ||
* @returns A `Rule` represents the component's variants. | ||
*/ | ||
export declare const variants: (variants: RuleCollection, options?: RuleOptions) => StyleCollection; | ||
export declare const variants: (variants: RuleCollection, options?: SelectorOptions) => Rule; | ||
export interface StateOptions extends SelectorOptions { | ||
inherit?: boolean; | ||
} | ||
/** | ||
* Defines component's states. | ||
* @param inherit `true` to inherit states from parent element -or- `false` to create independent states. | ||
* @returns A `StyleCollection` represents the component's states. | ||
* @returns A `Rule` represents the component's states. | ||
*/ | ||
export declare const states: (states: RuleCollection | ((inherit: boolean) => RuleCollection), inherit?: boolean, options?: RuleOptions) => StyleCollection; | ||
export declare const states: (states: RuleCollection | ((inherit: boolean) => RuleCollection), options?: StateOptions) => Rule; | ||
export declare const keyframes: (name: string, items: PropEx.Keyframes) => Rule; | ||
export declare const noRule: (...styles: StyleCollection[]) => Rule; | ||
export declare const emptyRule: () => Rule; | ||
export declare const fallbacks: (...styles: StyleCollection[]) => Rule; | ||
export declare const fontFace: (...styles: StyleCollection[]) => Rule; | ||
export declare const atGlobal: (...rules: RuleCollection[]) => Rule; | ||
export declare const atRoot: (...styles: StyleCollection[]) => Rule; | ||
export declare const isFirstChild: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotFirstChild: (...styles: StyleCollection[]) => Rule; | ||
export declare const isLastChild: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotLastChild: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNthChild: (step: number, offset: number, ...styles: StyleCollection[]) => Rule; | ||
export declare const isNotNthChild: (step: number, offset: number, ...styles: StyleCollection[]) => Rule; | ||
export declare const isNthLastChild: (step: number, offset: number, ...styles: StyleCollection[]) => Rule; | ||
export declare const isNotNthLastChild: (step: number, offset: number, ...styles: StyleCollection[]) => Rule; | ||
export declare const isActive: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotActive: (...styles: StyleCollection[]) => Rule; | ||
export declare const isFocus: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotFocus: (...styles: StyleCollection[]) => Rule; | ||
export declare const isFocusVisible: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotFocusVisible: (...styles: StyleCollection[]) => Rule; | ||
export declare const isHover: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotHover: (...styles: StyleCollection[]) => Rule; | ||
export declare const isEmpty: (...styles: StyleCollection[]) => Rule; | ||
export declare const isNotEmpty: (...styles: StyleCollection[]) => Rule; | ||
export declare const combinators: (combinator: Combinator, selectors: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const descendants: (selectors: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const children: (selectors: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const siblings: (selectors: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const nextSiblings: (selectors: SelectorCollection, styles: StyleCollection, options?: SelectorOptions) => Rule; | ||
export declare const iif: <T extends CssProps | Rule | Style>(condition: boolean, content: T) => T; | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `RuleEntry` represents the component's rule. | ||
*/ | ||
export declare const rule: (selectors: SelectorCollection, styles: StyleCollection) => RuleEntry; | ||
export declare const noRule: (styles: StyleCollection) => RuleEntry; | ||
export declare const emptyRule: () => RuleEntry; | ||
export declare const atRoot: (styles: StyleCollection) => RuleEntry; | ||
export declare const atGlobal: (styles: StyleCollection) => RuleEntry; | ||
export declare const fontFace: (styles: StyleCollection) => RuleEntry; | ||
export declare const isFirstChild: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotFirstChild: (styles: StyleCollection) => RuleEntry; | ||
export declare const isLastChild: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotLastChild: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNthChild: (step: number, offset: number, styles: StyleCollection) => RuleEntry; | ||
export declare const isNotNthChild: (step: number, offset: number, styles: StyleCollection) => RuleEntry; | ||
export declare const isNthLastChild: (step: number, offset: number, styles: StyleCollection) => RuleEntry; | ||
export declare const isNotNthLastChild: (step: number, offset: number, styles: StyleCollection) => RuleEntry; | ||
export declare const isActive: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotActive: (styles: StyleCollection) => RuleEntry; | ||
export declare const isFocus: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotFocus: (styles: StyleCollection) => RuleEntry; | ||
export declare const isFocusVisible: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotFocusVisible: (styles: StyleCollection) => RuleEntry; | ||
export declare const isHover: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotHover: (styles: StyleCollection) => RuleEntry; | ||
export declare const isEmpty: (styles: StyleCollection) => RuleEntry; | ||
export declare const isNotEmpty: (styles: StyleCollection) => RuleEntry; | ||
export declare const iif: <T extends Style | PropList>(condition: boolean, content: T) => T; | ||
/** | ||
* Escapes some sets of character in svg data, so it will be valid to be written in css. | ||
@@ -137,0 +153,0 @@ * @param svgData The raw svg data to be escaped. |
// jss: | ||
import { create as createJss, } from 'jss'; // base technology of our cssfn components | ||
// custom jss-plugins: | ||
import jssPluginGlobal from '@cssfn/jss-plugin-global'; | ||
import { default as jssPluginExtend, mergeStyle, } from '@cssfn/jss-plugin-extend'; | ||
import jssPluginNested from '@cssfn/jss-plugin-nested'; | ||
@@ -10,3 +8,13 @@ import jssPluginShort from '@cssfn/jss-plugin-short'; | ||
import jssPluginVendor from '@cssfn/jss-plugin-vendor'; | ||
// others libs: | ||
import { | ||
// parses: | ||
parseSelectors, | ||
// creates & tests: | ||
parentSelector, pseudoClassSelector, isSimpleSelector, isParentSelector, isClassOrPseudoClassSelector, isPseudoElementSelector, isNotPseudoElementSelector, isCombinator, createSelector, createSelectorList, isNotEmptySelectorEntry, isNotEmptySelector, isNotEmptySelectors, | ||
// renders: | ||
selectorsToString, | ||
// transforms: | ||
groupSelectors, groupSelector, ungroupSelector, | ||
// measures: | ||
calculateSpecificity, } from '@cssfn/css-selector'; | ||
import { pascalCase } from 'pascal-case'; // pascal-case support for jss | ||
@@ -61,5 +69,3 @@ import { camelCase } from 'camel-case'; // camel-case support for jss | ||
const customJss = createJss().setup({ createGenerateId, plugins: [ | ||
jssPluginGlobal(), | ||
jssPluginExtend(), | ||
jssPluginNested(), | ||
jssPluginNested((styles) => mergeStyles(styles)), | ||
jssPluginShort(), | ||
@@ -92,11 +98,94 @@ jssPluginCamelCase(), | ||
.map(([className, styles]) => ({ [className || '@global']: mergeStyles(styles) })) // convert each `[className, styles]` to `{ className : mergeStyles(styles) | null }` | ||
) ?? {}); | ||
) ?? emptyMergedStyle); | ||
}; | ||
// compositions: | ||
// processors: | ||
const isStyle = (object) => object && (typeof (object) === 'object') && !Array.isArray(object); | ||
const mergeLiteral = (style, newStyle) => { | ||
for (const [propName, newPropValue] of [ | ||
...Object.entries(newStyle), | ||
...Object.getOwnPropertySymbols(newStyle).map((sym) => [sym, newStyle[sym]]), | ||
]) { // loop through `newStyle`'s props | ||
if (!isStyle(newPropValue)) { | ||
// `newPropValue` is not a `Style` => unmergeable => add/overwrite `newPropValue` into `style`: | ||
delete style[propName]; // delete the old prop (if any), so the new prop always placed at the end of LiteralObject | ||
style[propName] = newPropValue; // add/overwrite | ||
} | ||
else { | ||
// `newPropValue` is a `Style` => possibility to merge with `currentPropValue` | ||
const currentPropValue = style[propName]; | ||
if (!isStyle(currentPropValue)) { | ||
// `currentPropValue` is not a `Style` => unmergeable => add/overwrite `newPropValue` into `style`: | ||
delete style[propName]; // delete the old prop (if any), so the new prop always placed at the end of LiteralObject | ||
style[propName] = newPropValue; // add/overwrite | ||
} | ||
else { | ||
// both `newPropValue` & `currentPropValue` are `Style` => merge them recursively (deeply): | ||
const currentValueClone = { ...currentPropValue }; // clone the `currentPropValue` to avoid side effect, because the `currentPropValue` is not **the primary object** we're working on | ||
mergeLiteral(currentValueClone, newPropValue); | ||
// merging style prop no need to rearrange the prop position | ||
style[propName] = currentValueClone; // set the mutated `currentValueClone` back to `style` | ||
} // if | ||
} // if | ||
} // for | ||
}; | ||
const mergeNested = (style) => { | ||
//#region group (nested) Rule(s) by selector name | ||
const groupByNested = (Object.getOwnPropertySymbols(style) | ||
.reduce((accum, sym) => { | ||
const nestedSelector = sym.description ?? ''; | ||
if ( | ||
// nested rules: | ||
nestedSelector.includes('&') | ||
|| | ||
// conditional rules & globals: | ||
['@media', '@supports', '@document', '@global'].some((at) => nestedSelector.startsWith(at))) { | ||
let group = accum.get(nestedSelector); // get an existing collector | ||
if (!group) | ||
accum.set(nestedSelector, group = []); // create a new collector | ||
group.push(sym); | ||
} // if | ||
return accum; | ||
}, new Map())); | ||
//#endregion group (nested) Rule(s) by selector name | ||
//#region merge duplicates (nested) Rule(s) to unique ones | ||
for (const group of Array.from(groupByNested.values())) { | ||
if (group.length <= 1) | ||
continue; // filter out groups with single/no member | ||
const mergedStyles = mergeStyles(group.map((sym) => style[sym])); | ||
if (mergedStyles) { | ||
// update last member | ||
style[group[group.length - 1]] = mergedStyles; // merge all member's style to the last member | ||
} | ||
else { | ||
// mergedStyles is empty => delete last member | ||
delete style[group[group.length - 1]]; | ||
} // if | ||
for (const sym of group.slice(0, -1)) | ||
delete style[sym]; // delete first member to second last member | ||
} // for | ||
//#endregion merge duplicates (nested) Rule to unique ones | ||
//#region merge only_parentSelector into current style | ||
const parentSelector = groupByNested.get('&')?.pop(); // remove & get the last member in parentSelector group | ||
if (parentSelector) { | ||
const parentStyles = style[parentSelector]; | ||
const mergedParentStyles = mergeStyles(parentStyles); | ||
if (mergedParentStyles) { | ||
mergeLiteral(style, mergedParentStyles); // merge into current style | ||
delete style[parentSelector]; // merged => delete source | ||
} // if | ||
} // if | ||
//#endregion merge only_parentSelector into current style | ||
return style; | ||
}; | ||
// prevents JSS to clone the CSSFN Style (because the symbol props are not copied) | ||
class MergedStyle { | ||
constructor(style) { | ||
if (style) | ||
Object.assign(this, style); | ||
} | ||
} | ||
; | ||
const emptyMergedStyle = new MergedStyle(); | ||
Object.seal(emptyMergedStyle); | ||
/** | ||
* Defines the (sub) component's composition. | ||
* @returns A `StyleCollection` represents the (sub) component's composition. | ||
*/ | ||
export const composition = (styles) => styles; | ||
/** | ||
* Merges the (sub) component's composition to single `Style`. | ||
@@ -123,10 +212,18 @@ * @returns A `Style` represents the merged (sub) component's composition | ||
return null; // `null` or `undefined` => return `null` | ||
return styleValue; | ||
const mergedStyles = new MergedStyle(styleValue); | ||
mergeNested(mergedStyles); | ||
// do not return an empty style, instead return null: | ||
if ((!Object.keys(mergedStyles).length) && (!Object.getOwnPropertySymbols(mergedStyles).length)) | ||
return null; // an empty object => return `null` | ||
// return non empty style: | ||
return mergedStyles; | ||
} // if | ||
const mergedStyles = {}; | ||
for (const subStyles of styles) { | ||
const mergedStyles = new MergedStyle(); | ||
for (const subStyles of styles) { // shallow iterating array | ||
const subStyleValue = (Array.isArray(subStyles) | ||
? | ||
// deep iterating array | ||
mergeStyles(subStyles) // an array => ProductOrFactoryDeepArray<OptionalOrFalse<Style>> => recursively `mergeStyles()` | ||
: | ||
// final element => might be a function or a product | ||
( | ||
@@ -142,8 +239,311 @@ // not an array => ProductOrFactory<OptionalOrFalse<Style>> | ||
continue; // `null` or `undefined` => skip | ||
mergeStyle(mergedStyles, subStyleValue); | ||
// merge current style to single big style (string props + symbol props): | ||
mergeLiteral(mergedStyles, subStyleValue); | ||
} // for | ||
if (Object.keys(mergedStyles).length === 0) | ||
mergeNested(mergedStyles); | ||
// do not return an empty style, instead return null: | ||
if ((!Object.keys(mergedStyles).length) && (!Object.getOwnPropertySymbols(mergedStyles).length)) | ||
return null; // an empty object => return `null` | ||
// return non empty style: | ||
return mergedStyles; | ||
}; | ||
const nthChildNSelector = pseudoClassSelector('nth-child', 'n'); | ||
const adjustSpecificityWeight = (selectorList, minSpecificityWeight, maxSpecificityWeight) => { | ||
if ((minSpecificityWeight == null) | ||
&& | ||
(maxSpecificityWeight == null)) | ||
return selectorList; // nothing to adjust | ||
const selectorListBySpecificityWeightStatus = selectorList.map((selector) => selector.filter(isNotEmptySelectorEntry)).reduce((accum, selector) => { | ||
const [specificityWeight, weightStatus] = (() => { | ||
const specificityWeight = calculateSpecificity(selector)[1]; | ||
if ((maxSpecificityWeight !== null) && (specificityWeight > maxSpecificityWeight)) { | ||
return [specificityWeight, 1 /* TooBig */]; | ||
} // if | ||
if ((minSpecificityWeight !== null) && (specificityWeight < minSpecificityWeight)) { | ||
return [specificityWeight, 2 /* TooSmall */]; | ||
} // if | ||
return [specificityWeight, 0 /* Fit */]; | ||
})(); | ||
let group = accum.get(weightStatus); // get an existing collector | ||
if (!group) | ||
accum.set(weightStatus, group = []); // create a new collector | ||
group.push({ selector, specificityWeight }); | ||
return accum; | ||
}, new Map()); | ||
//#endregion group selectors by specificity weight status | ||
const fitSelectors = selectorListBySpecificityWeightStatus.get(0 /* Fit */) ?? []; | ||
const tooBigSelectors = selectorListBySpecificityWeightStatus.get(1 /* TooBig */) ?? []; | ||
const tooSmallSelectors = selectorListBySpecificityWeightStatus.get(2 /* TooSmall */) ?? []; | ||
return createSelectorList(...fitSelectors.map((group) => group.selector), ...tooBigSelectors.flatMap((group) => { | ||
const reversedSelector = group.selector.reverse(); // reverse & mutate the current `group.selector` array | ||
const { reducedSelector: reversedReducedSelector, remaining: remainingSpecificityWeight } = (reversedSelector.slice(0) // clone the `reversedSelector` because the `reduce()` uses `splice()` to break the iteration | ||
.reduce((accum, selectorEntry, index, array) => { | ||
if (accum.remaining <= 0) { | ||
array.splice(1); // eject early by mutating iterated copy - it's okay to **mutate** the `array` because it already cloned at `slice(0)` | ||
return accum; | ||
} // if | ||
if (isSimpleSelector(selectorEntry)) { | ||
const [ | ||
/* | ||
selector tokens: | ||
'&' = parent selector | ||
'*' = universal selector | ||
'[' = attribute selector | ||
'' = element selector | ||
'#' = ID selector | ||
'.' = class selector | ||
':' = pseudo class selector | ||
'::' = pseudo element selector | ||
*/ | ||
selectorToken, | ||
/* | ||
selector name: | ||
string = the name of [element, ID, class, pseudo class, pseudo element] selector | ||
*/ | ||
selectorName, | ||
/* | ||
selector parameter(s): | ||
string = the parameter of pseudo class selector, eg: nth-child(2n+3) => '2n+3' | ||
array = [name, operator, value, options] of attribute selector, eg: [data-msg*="you & me" i] => ['data-msg', '*=', 'you & me', 'i'] | ||
SelectorList = nested selector(s) of pseudo class [:is(...), :where(...), :not(...)] | ||
*/ | ||
// selectorParams, | ||
] = selectorEntry; | ||
if (selectorToken === ':') { | ||
switch (selectorName) { | ||
case 'is': | ||
case 'not': | ||
case 'has': | ||
const specificityWeight = calculateSpecificity([selectorEntry])[1]; | ||
accum.remaining -= specificityWeight; // reduce the counter | ||
break; | ||
case 'where': | ||
break; // don't reduce the counter | ||
default: | ||
accum.remaining--; // reduce the counter | ||
} // switch | ||
} | ||
else if (['.', '[',].includes(selectorToken)) { | ||
accum.remaining--; // reduce the counter | ||
} // if | ||
} // if | ||
accum.reducedSelector.push(selectorEntry); | ||
return accum; | ||
}, { | ||
remaining: (group.specificityWeight - (maxSpecificityWeight ?? group.specificityWeight)), | ||
reducedSelector: [], | ||
})); | ||
const [whereSelector, ...pseudoElmSelectors] = groupSelector(reversedReducedSelector.reverse(), { selectorName: 'where' }); | ||
whereSelector.unshift(...reversedSelector.slice(reversedReducedSelector.length).reverse()); | ||
whereSelector.push(...(new Array((remainingSpecificityWeight < 0) ? -remainingSpecificityWeight : 0)).fill(nthChildNSelector // or use `nth-child(n)` | ||
)); | ||
return createSelectorList(whereSelector, ...pseudoElmSelectors); | ||
}), ...tooSmallSelectors.map((group) => createSelector(...group.selector, ...(new Array((minSpecificityWeight ?? 1) - group.specificityWeight)).fill(group.selector | ||
.filter(isClassOrPseudoClassSelector) // only interested to class selector -or- pseudo class selector | ||
.filter((simpleSelector) => { | ||
const [ | ||
/* | ||
selector tokens: | ||
'&' = parent selector | ||
'*' = universal selector | ||
'[' = attribute selector | ||
'' = element selector | ||
'#' = ID selector | ||
'.' = class selector | ||
':' = pseudo class selector | ||
'::' = pseudo element selector | ||
*/ | ||
// selectorToken | ||
, | ||
/* | ||
selector name: | ||
string = the name of [element, ID, class, pseudo class, pseudo element] selector | ||
*/ | ||
// selectorName | ||
, | ||
/* | ||
selector parameter(s): | ||
string = the parameter of pseudo class selector, eg: nth-child(2n+3) => '2n+3' | ||
array = [name, operator, value, options] of attribute selector, eg: [data-msg*="you & me" i] => ['data-msg', '*=', 'you & me', 'i'] | ||
SelectorList = nested selector(s) of pseudo class [:is(...), :where(...), :not(...)] | ||
*/ | ||
selectorParams,] = simpleSelector; | ||
return (selectorParams === undefined); | ||
}) | ||
.pop() // repeats the last selector until minSpecificityWeight satisfied | ||
?? | ||
nthChildNSelector // or use `nth-child(n)` | ||
)))); | ||
}; | ||
const defaultSelectorOptions = { | ||
groupSelectors: true, | ||
specificityWeight: null, | ||
minSpecificityWeight: null, | ||
maxSpecificityWeight: null, | ||
}; | ||
export const mergeSelectors = (selectorList, options = defaultSelectorOptions) => { | ||
const { groupSelectors: doGroupSelectors = defaultSelectorOptions.groupSelectors, specificityWeight, } = options; | ||
const minSpecificityWeight = specificityWeight ?? options.minSpecificityWeight ?? null; | ||
const maxSpecificityWeight = specificityWeight ?? options.maxSpecificityWeight ?? null; | ||
if (!doGroupSelectors // do not perform grouping | ||
&& | ||
(minSpecificityWeight === null) && (maxSpecificityWeight === null) // do not perform transform | ||
) | ||
return selectorList; // nothing to do | ||
const normalizedSelectorList = (selectorList | ||
.flatMap((selector) => ungroupSelector(selector)) | ||
.filter(isNotEmptySelector)); | ||
if ((!doGroupSelectors || (normalizedSelectorList.length <= 1)) // do not perform grouping || only singular => nothing to group | ||
&& | ||
(minSpecificityWeight === null) && (maxSpecificityWeight === null) // do not perform transform | ||
) | ||
return normalizedSelectorList; // nothing to do | ||
// transform: | ||
const adjustedSelectorList = adjustSpecificityWeight(normalizedSelectorList, minSpecificityWeight, maxSpecificityWeight); | ||
if ((!doGroupSelectors || (adjustedSelectorList.length <= 1)) // do not perform grouping || only singular => nothing to group | ||
) | ||
return adjustedSelectorList; // nothing to do | ||
const selectorListByParentPosition = adjustedSelectorList.map((selector) => selector.filter(isNotEmptySelectorEntry)).reduce((accum, selector) => { | ||
const position = (() => { | ||
const hasFirstParent = (() => { | ||
if (selector.length < 1) | ||
return false; // at least 1 entry must exist, for the first_parent | ||
const firstSelectorEntry = selector[0]; // take the first entry | ||
return isParentSelector(firstSelectorEntry); // the entry must be ParentSelector | ||
})(); | ||
const onlyParent = hasFirstParent && (selector.length === 1); | ||
if (onlyParent) | ||
return 0 /* OnlyParent */; | ||
const hasMiddleParent = (() => { | ||
if (selector.length < 3) | ||
return false; // at least 3 entry must exist, the first & last are already reserved, the middle one is the middle_parent | ||
for (let index = 1, maxIndex = (selector.length - 2); index <= maxIndex; index++) { | ||
const middleSelectorEntry = selector[index]; // take the 2nd_first_entry until the 2nd_last_entry | ||
if (isParentSelector(middleSelectorEntry)) | ||
return true; // the entry must be ParentSelector, otherwise skip to next | ||
} // for | ||
return false; // ran out of iterator => not found | ||
})(); | ||
const hasLastParent = (() => { | ||
const length = selector.length; | ||
if (length < 2) | ||
return false; // at least 2 entry must exist, the first is already reserved, the last one is the last_parent | ||
const lastSelectorEntry = selector[length - 1]; // take the last entry | ||
return isParentSelector(lastSelectorEntry); // the entry must be ParentSelector | ||
})(); | ||
const onlyBeginParent = hasFirstParent && !hasMiddleParent && !hasLastParent; | ||
if (onlyBeginParent) | ||
return 1 /* OnlyBeginParent */; | ||
const onlyEndParent = !hasFirstParent && !hasMiddleParent && hasLastParent; | ||
if (onlyEndParent) | ||
return 2 /* OnlyEndParent */; | ||
return 3 /* RandomParent */; | ||
})(); | ||
let group = accum.get(position); // get an existing collector | ||
if (!group) | ||
accum.set(position, group = []); // create a new collector | ||
group.push(selector); | ||
return accum; | ||
}, new Map()); | ||
//#endregion group selectors by parent position | ||
const onlyParentSelectorList = selectorListByParentPosition.get(0 /* OnlyParent */) ?? []; | ||
const onlyBeginParentSelectorList = selectorListByParentPosition.get(1 /* OnlyBeginParent */) ?? []; | ||
const onlyEndParentSelectorList = selectorListByParentPosition.get(2 /* OnlyEndParent */) ?? []; | ||
const randomParentSelectorList = selectorListByParentPosition.get(3 /* RandomParent */) ?? []; | ||
const createGroupByCombinator = (fetch) => (accum, selector) => { | ||
const combinator = fetch(selector); | ||
let group = accum.get(combinator); // get an existing collector | ||
if (!group) | ||
accum.set(combinator, group = []); // create a new collector | ||
group.push(selector); | ||
return accum; | ||
}; | ||
const groupedSelectorList = createSelectorList( | ||
// only ParentSelector | ||
// & | ||
!!onlyParentSelectorList.length && (onlyParentSelectorList[0] // just take the first one, the rest are guaranteed to be the same | ||
), | ||
// ParentSelector at beginning | ||
// &aaa | ||
// &:is(aaa, bbb, ccc) | ||
...(() => { | ||
if (onlyBeginParentSelectorList.length <= 1) | ||
return onlyBeginParentSelectorList; // only contain one/no Selector, no need to group | ||
//#region group selectors by combinator | ||
const selectorListByCombinator = onlyBeginParentSelectorList.reduce(createGroupByCombinator((selector) => { | ||
if (selector.length >= 2) { // at least 2 entry must exist, for the first_parent followed by combinator | ||
const secondSelectorEntry = selector[1]; // take the first_second entry | ||
if (isCombinator(secondSelectorEntry)) { // the entry must be the same as combinator | ||
return secondSelectorEntry; | ||
} // if | ||
} // if | ||
return null; // ungroupable | ||
}), new Map()); | ||
//#endregion group selectors by combinator | ||
return Array.from(selectorListByCombinator.entries()).flatMap(([combinator, selectors]) => { | ||
if (selectors.length <= 1) | ||
return selectors; // only contain one/no Selector, no need to group | ||
if (selectors.filter((selector) => selector.every(isNotPseudoElementSelector)).length <= 1) | ||
return selectors; // only contain one/no Selector without ::pseudo-element, no need to group | ||
const [isSelector, ...pseudoElmSelectors] = groupSelectors(selectors | ||
.filter(isNotEmptySelector) // remove empty Selector(s) in SelectorList | ||
.map((selector) => selector.slice((combinator | ||
? | ||
2 // remove the first_parent & combinator | ||
: | ||
1 // remove the first_parent | ||
) | ||
+ | ||
(selector.some(isPseudoElementSelector) ? -1 : 0) // exception for ::pseudo-element => do not remove the first_parent | ||
)), { selectorName: 'is' }); | ||
return createSelectorList(isNotEmptySelector(isSelector) && createSelector(parentSelector(), // add a ParentSelector before :is(...) | ||
combinator, // add a Combinator (if any) before :is(...) | ||
...isSelector), ...pseudoElmSelectors); | ||
}); | ||
})(), | ||
// ParentSelector at end | ||
// aaa& | ||
// :is(aaa, bbb, ccc)& | ||
...(() => { | ||
if (onlyEndParentSelectorList.length <= 1) | ||
return onlyEndParentSelectorList; // only contain one/no Selector, no need to group | ||
//#region group selectors by combinator | ||
const selectorListByCombinator = onlyEndParentSelectorList.reduce(createGroupByCombinator((selector) => { | ||
const length = selector.length; | ||
if (length >= 2) { // at least 2 entry must exist, for the combinator followed by last_parent | ||
const secondSelectorEntry = selector[length - 2]; // take the last_second entry | ||
if (isCombinator(secondSelectorEntry)) { // the entry must be the same as combinator | ||
return secondSelectorEntry; | ||
} // if | ||
} // if | ||
return null; // ungroupable | ||
}), new Map()); | ||
//#endregion group selectors by combinator | ||
return Array.from(selectorListByCombinator.entries()).flatMap(([combinator, selectors]) => { | ||
if (selectors.length <= 1) | ||
return selectors; // only contain one/no Selector, no need to group | ||
if (selectors.filter((selector) => selector.every(isNotPseudoElementSelector)).length <= 1) | ||
return selectors; // only contain one/no Selector without ::pseudo-element, no need to group | ||
const [isSelector, ...pseudoElmSelectors] = groupSelectors(selectors | ||
.filter(isNotEmptySelector) // remove empty Selector(s) in SelectorList | ||
.map((selector) => selector.slice(0, (combinator | ||
? | ||
-2 // remove the combinator & last_parent | ||
: | ||
-1 // remove the last_parent | ||
) | ||
+ | ||
(selector.some(isPseudoElementSelector) ? 1 : 0) // exception for ::pseudo-element => do not remove the last_parent | ||
)), { selectorName: 'is' }); | ||
return createSelectorList(isNotEmptySelector(isSelector) && createSelector(...isSelector, // :is(...) | ||
combinator, // add a Combinator (if any) after :is(...) | ||
parentSelector()), ...pseudoElmSelectors); | ||
}); | ||
})(), | ||
// parent at random | ||
// a&aa, bb&b, c&c&c | ||
...randomParentSelectorList); | ||
return groupedSelectorList; | ||
}; | ||
// compositions: | ||
/** | ||
@@ -153,3 +553,3 @@ * Defines the additional component's composition. | ||
*/ | ||
export const compositionOf = (className, styles) => [ | ||
export const compositionOf = (className, ...styles) => [ | ||
className, | ||
@@ -163,3 +563,3 @@ styles | ||
*/ | ||
export const mainComposition = (styles) => compositionOf('main', styles); | ||
export const mainComposition = (...styles) => compositionOf('main', ...styles); | ||
/** | ||
@@ -169,372 +569,233 @@ * Defines the global style applied to a whole document. | ||
*/ | ||
export const globalDef = (ruleCollection) => compositionOf('', [rules(ruleCollection)]); | ||
export const imports = (styles) => composition(styles); | ||
// layouts: | ||
export const globalDef = (...rules) => compositionOf('', ...rules); | ||
// styles: | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines the (sub) component's composition. | ||
* @returns A `Rule` represents the (sub) component's composition. | ||
*/ | ||
export const composition = (...styles) => noRule(...styles); | ||
/** | ||
* Defines component's style. | ||
* @returns A `Rule` represents the component's style. | ||
*/ | ||
export const style = (style) => noRule(style); | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines component's layout. | ||
* @returns A `Style` represents the component's layout. | ||
* @returns A `Rule` represents the component's layout. | ||
*/ | ||
export const layout = (style) => style; | ||
export const layout = (style) => noRule(style); | ||
/** | ||
* Defines component's variable(s). | ||
* @returns A `Style` represents the component's variable(s). | ||
* @returns A `Rule` represents the component's variable(s). | ||
*/ | ||
export const vars = (items) => items; | ||
const defaultCombinatorOptions = { | ||
groupSelectors: true, | ||
export const vars = (items) => noRule(items); | ||
export const imports = (...styles) => noRule(...styles); | ||
// rules: | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `Rule` represents the component's rule. | ||
*/ | ||
export const rule = (rules, styles, options = defaultSelectorOptions) => { | ||
const rulesString = (flat(rules) | ||
.filter((rule) => !!rule)); | ||
const rulesByTypes = rulesString.reduce((accum, rule) => { | ||
let ruleType = (() => { | ||
if (rule.startsWith('@')) | ||
return 1 /* AtRule */; | ||
if (rule.startsWith(' ')) | ||
return 2 /* PropRule */; | ||
if (rule.includes('&')) | ||
return 0 /* SelectorRule */; | ||
return null; | ||
})(); | ||
switch (ruleType) { | ||
case 2 /* PropRule */: | ||
rule = rule.slice(1); | ||
break; | ||
case null: | ||
ruleType = 0 /* SelectorRule */; | ||
rule = `&${rule}`; | ||
break; | ||
} // switch | ||
let group = accum.get(ruleType); // get an existing collector | ||
if (!group) | ||
accum.set(ruleType, group = []); // create a new collector | ||
group.push(rule); | ||
return accum; | ||
}, new Map()); | ||
const selectorList = ((rulesByTypes.get(0 /* SelectorRule */) ?? []) | ||
.flatMap((selector) => { | ||
const selectorList = parseSelectors(selector); | ||
if (!selectorList) | ||
throw Error(`parse selector error: ${selector}`); | ||
return selectorList; | ||
}) | ||
.filter(isNotEmptySelector)); | ||
const mergedSelectorList = mergeSelectors(selectorList, options); | ||
return { | ||
...(isNotEmptySelectors(mergedSelectorList) ? { | ||
[Symbol(selectorsToString(mergedSelectorList))]: styles | ||
} : {}), | ||
...Object.fromEntries([ | ||
...(rulesByTypes.get(1 /* AtRule */) ?? []), | ||
...(rulesByTypes.get(2 /* PropRule */) ?? []), | ||
].map((rule) => [ | ||
Symbol(rule), | ||
styles | ||
])), | ||
}; | ||
}; | ||
export const combinators = (combinator, selectors, styles, options = defaultCombinatorOptions) => { | ||
const { groupSelectors = defaultCombinatorOptions.groupSelectors, } = options; | ||
const withCombinator = `&${combinator}`; | ||
const combiSelectors = flat(selectors).map((selector) => { | ||
if (!selector) | ||
selector = '*'; // empty selector => match any element | ||
// if (selector === '&') return selector; // no children => the parent itself | ||
if (selector.includes('&')) | ||
return selector; // custom combinator | ||
if (((combinator === ' ') || (combinator === '>')) && selector.startsWith('::')) | ||
return `&${selector}`; // pseudo element => attach the parent itself (for descendants & children) | ||
return `${withCombinator}${selector}`; | ||
}); | ||
if (!combiSelectors.length) | ||
return {}; // no selector => return empty | ||
const mergedStyles = mergeStyles(styles); // merge the `styles` to single `Style`, for making JSS understand | ||
if (!mergedStyles) | ||
return {}; // no style => return empty | ||
if (groupSelectors) { | ||
if (combiSelectors.length === 1) | ||
return { | ||
[combiSelectors[0]]: mergedStyles, | ||
}; | ||
const selectorsGroups = combiSelectors.map((selector) => { | ||
const withCombi = selector.startsWith(withCombinator); | ||
if (withCombi) | ||
return { withCombi: selector }; | ||
const onlyBeginAmp = (selector.lastIndexOf('&') === 0); | ||
if (onlyBeginAmp) | ||
return { begAmp: selector }; | ||
const onlyEndAmp = (selector.indexOf('&') === (selector.length - 1)); | ||
if (onlyEndAmp) | ||
return { endAmp: selector }; | ||
return { other: selector }; | ||
}); | ||
const withCombiSelectors = selectorsGroups.filter((group) => !!group.withCombi).map((group) => group.withCombi); | ||
const begAmpSelectors = selectorsGroups.filter((group) => !!group.begAmp).map((group) => group.begAmp); | ||
const endAmpSelectors = selectorsGroups.filter((group) => !!group.endAmp).map((group) => group.endAmp); | ||
const ungroupableSelectors = selectorsGroups.filter((group) => !!group.other).map((group) => group.other); | ||
return { | ||
...(withCombiSelectors.length ? { | ||
[(withCombiSelectors.length === 1) | ||
? | ||
withCombiSelectors[0] | ||
: | ||
`${withCombinator}:is(${withCombiSelectors.map((selector) => selector.slice(withCombinator.length)).join(',')})`]: mergedStyles, | ||
} : {}), | ||
...(begAmpSelectors.length ? { | ||
[(begAmpSelectors.length === 1) | ||
? | ||
begAmpSelectors[0] | ||
: | ||
`&:is(${begAmpSelectors.map((selector) => selector.slice(1)).join(',')})`]: mergedStyles, | ||
} : {}), | ||
...(endAmpSelectors.length ? { | ||
[(endAmpSelectors.length === 1) | ||
? | ||
endAmpSelectors[0] | ||
: | ||
`:is(${endAmpSelectors.map((selector) => selector.slice(0, -1)).join(',')})&`]: mergedStyles, | ||
} : {}), | ||
...(ungroupableSelectors.length ? { | ||
[ungroupableSelectors.join(',')]: mergedStyles, | ||
} : {}), | ||
}; | ||
} | ||
else { | ||
return Object.fromEntries(combiSelectors | ||
.map((combiSelector) => [combiSelector, mergedStyles])); | ||
} // if | ||
// rule groups: | ||
export const rules = (rules, options = defaultSelectorOptions) => { | ||
const result = (flat(rules) | ||
.filter((rule) => !!rule) | ||
.flatMap((ruleProductOrFactory) => { | ||
if (typeof (ruleProductOrFactory) === 'function') | ||
return [ruleProductOrFactory()]; | ||
return [ruleProductOrFactory]; | ||
}) | ||
.filter((optionalRule) => !!optionalRule)); | ||
if (!options) | ||
return Object.assign({}, ...result); | ||
return Object.assign({}, ...result | ||
.flatMap((rule) => Object.getOwnPropertySymbols(rule).map((sym) => [sym.description ?? '', rule[sym]])) | ||
.map(([selectors, styles]) => rule(selectors, styles, options))); | ||
}; | ||
export const descendants = (selectors, styles, options = defaultCombinatorOptions) => combinators(' ', selectors, styles, options); | ||
export const children = (selectors, styles, options = defaultCombinatorOptions) => combinators('>', selectors, styles, options); | ||
export const siblings = (selectors, styles, options = defaultCombinatorOptions) => combinators('~', selectors, styles, options); | ||
export const nextSiblings = (selectors, styles, options = defaultCombinatorOptions) => combinators('+', selectors, styles, options); | ||
const defaultRuleOptions = { | ||
minSpecificityWeight: 0, | ||
}; | ||
export const rules = (ruleCollection, options = defaultRuleOptions) => { | ||
const { minSpecificityWeight = defaultRuleOptions.minSpecificityWeight, } = options; | ||
return composition((() => { | ||
const noSelectors = []; | ||
return [ | ||
...(Array.isArray(ruleCollection) ? ruleCollection : [ruleCollection]) | ||
.flatMap((ruleEntrySourceList) => { | ||
const isOptionalString = (value) => { | ||
if (value === null) | ||
return true; // optional `null` | ||
if (value === undefined) | ||
return true; // optional `undefined` | ||
if (value === false) | ||
return true; // optional `false` | ||
return ((typeof value) === 'string'); | ||
}; | ||
const isOptionalStringDeepArr = (value) => { | ||
if (!Array.isArray(value)) | ||
return false; | ||
const nonOptionalStringItems = value.filter((v) => !isOptionalString(v)); | ||
if (nonOptionalStringItems.length === 0) | ||
return true; | ||
for (const nonOptionalStringItem of nonOptionalStringItems) { | ||
if (!isOptionalStringDeepArr(nonOptionalStringItem)) | ||
return false; | ||
} // for | ||
return true; | ||
}; | ||
const isOptionalSelector = (value) => isOptionalString(value); | ||
const isOptionalSelectorDeepArr = (value) => isOptionalStringDeepArr(value); | ||
const isOptionalStyleOrFactory = (value) => { | ||
if (value === null) | ||
return true; // optional `null` | ||
if (value === undefined) | ||
return true; // optional `undefined` | ||
return (value | ||
&& | ||
(((typeof (value) === 'object') && !Array.isArray(value)) // literal object => `Style` | ||
|| | ||
(typeof (value) === 'function') // function => `Factory<Style>` | ||
)); | ||
}; | ||
const isOptionalStyleOrFactoryDeepArr = (value) => { | ||
if (!Array.isArray(value)) | ||
return false; | ||
const nonStyleOrFactoryItems = value.filter((v) => !isOptionalStyleOrFactory(v)); | ||
if (nonStyleOrFactoryItems.length === 0) | ||
return true; | ||
for (const nonStyleOrFactoryItem of nonStyleOrFactoryItems) { | ||
if (!isOptionalStyleOrFactoryDeepArr(nonStyleOrFactoryItem)) | ||
return false; | ||
} // for | ||
return true; | ||
}; | ||
const isOptionalRuleEntry = (value) => { | ||
if (value === null) | ||
return true; // optional `null` | ||
if (value === undefined) | ||
return true; // optional `undefined` | ||
if (value === false) | ||
return true; // optional `false` | ||
if (value.length !== 2) | ||
return false; // not a tuple => not a `RuleEntry` | ||
const [first, second] = value; | ||
/* | ||
the first element must be `SelectorCollection`: | ||
* `OptionalOrFalse<Selector>` | ||
* DeepArrayOf< `OptionalOrFalse<Selector>` > | ||
* empty array | ||
*/ | ||
// and | ||
/* | ||
the second element must be `StyleCollection`: | ||
* `OptionalOrFalse<Style>` | `Factory<OptionalOrFalse<Style>>` | ||
* DeepArrayOf< `OptionalOrFalse<Style> | Factory<OptionalOrFalse<Style>>` > | ||
* empty array | ||
*/ | ||
return ((isOptionalSelector(first) | ||
|| | ||
isOptionalSelectorDeepArr(first)) | ||
&& | ||
(isOptionalStyleOrFactory(second) | ||
|| | ||
isOptionalStyleOrFactoryDeepArr(second))); | ||
}; | ||
if (typeof (ruleEntrySourceList) === 'function') | ||
return [ruleEntrySourceList()]; | ||
if (isOptionalRuleEntry(ruleEntrySourceList)) | ||
return [ruleEntrySourceList]; | ||
return ruleEntrySourceList.map((ruleEntrySource) => (typeof (ruleEntrySource) === 'function') ? ruleEntrySource() : ruleEntrySource); | ||
}) | ||
.filter((optionalRuleEntry) => !!optionalRuleEntry) | ||
.map(([selectors, styles]) => { | ||
let nestedSelectors = flat(selectors).filter((selector) => !!selector).map((selector) => { | ||
if (selector.startsWith('@')) | ||
return selector; // for `@media` | ||
if (selector.includes('&')) | ||
return selector; // &.foo .boo& .foo&.boo | ||
// if (selector.startsWith('.') || selector.startsWith(':') || selector.startsWith('#') || (selector === '*')) return `&${selector}`; | ||
return `&${selector}`; | ||
}); | ||
if (minSpecificityWeight >= 2) { | ||
nestedSelectors = nestedSelectors.map((nestedSelector) => { | ||
if (nestedSelector === '&') | ||
return nestedSelector; // zero specificity => no change | ||
// one/more specificities found => increase the specificity weight until reaches `minSpecificityWeight` | ||
// calculate the specificity weight: | ||
// `.realClassName` or `:pseudoClassName` (without parameters): | ||
const classes = nestedSelector.match(/(\.|:(?!(is|not)(?![\w-])))[\w-]+/gmi); // count the `.RealClass` and `:PseudoClass` but not `:is` or `:not` | ||
const specificityWeight = classes?.length ?? 0; | ||
const missSpecificityWeight = minSpecificityWeight - specificityWeight; | ||
// the specificity weight was meet the minimum specificity required => no change: | ||
if (missSpecificityWeight <= 0) | ||
return nestedSelector; | ||
// the specificity weight is less than the minimum specificity required => increase the specificity: | ||
return `${nestedSelector}${(new Array(missSpecificityWeight)).fill((() => { | ||
const lastClass = classes?.[classes.length - 1]; | ||
if (lastClass) { | ||
// the last word (without parameters): | ||
if (nestedSelector.length === (nestedSelector.lastIndexOf(lastClass) + lastClass.length)) | ||
return lastClass; // `.RealClass` or `:PseudoClass` without parameters | ||
} // if | ||
// use a **hacky class name** to increase the specificity: | ||
return ':not(._)'; | ||
})()).join('')}`; | ||
}); | ||
} // if | ||
if (nestedSelectors.includes('&')) { // contains one/more selectors with zero specificity | ||
nestedSelectors = nestedSelectors.filter((nestedSelector) => (nestedSelector !== '&')); // filter out selectors with zero specificity | ||
noSelectors.push(styles); // add styles with zero specificity | ||
} // if | ||
return [nestedSelectors, styles]; | ||
}) | ||
.filter(([nestedSelectors]) => (nestedSelectors.length > 0)) // filter out empty `nestedSelectors` | ||
.map(([nestedSelectors, styles]) => [ | ||
nestedSelectors, | ||
mergeStyles(styles) // merge the `styles` to single `Style`, for making JSS understand | ||
]) | ||
.filter((tuple) => !!tuple[1]) // filter out empty `mergedStyles` | ||
.map(([nestedSelectors, mergedStyles]) => { | ||
const selectorsGroups = nestedSelectors.map((selector) => { | ||
const onlyBeginAmp = (selector.lastIndexOf('&') === 0); | ||
if (onlyBeginAmp) | ||
return { begAmp: selector }; | ||
const onlyEndAmp = (selector.indexOf('&') === (selector.length - 1)); | ||
if (onlyEndAmp) | ||
return { endAmp: selector }; | ||
return { other: selector }; | ||
}); | ||
const begAmpSelectors = selectorsGroups.filter((group) => !!group.begAmp).map((group) => group.begAmp); | ||
const endAmpSelectors = selectorsGroups.filter((group) => !!group.endAmp).map((group) => group.endAmp); | ||
const ungroupableSelectors = selectorsGroups.filter((group) => !!group.other).map((group) => group.other); | ||
return { | ||
...(begAmpSelectors.length ? { | ||
[(begAmpSelectors.length === 1) | ||
? | ||
begAmpSelectors[0] | ||
: | ||
`&:is(${begAmpSelectors.map((selector) => selector.slice(1)).join(',')})`]: mergedStyles, | ||
} : {}), | ||
...(endAmpSelectors.length ? { | ||
[(endAmpSelectors.length === 1) | ||
? | ||
endAmpSelectors[0] | ||
: | ||
`:is(${endAmpSelectors.map((selector) => selector.slice(0, -1)).join(',')})&`]: mergedStyles, | ||
} : {}), | ||
...(ungroupableSelectors.length ? { | ||
[ungroupableSelectors.join(',')]: mergedStyles, | ||
} : {}), | ||
}; | ||
}), | ||
...noSelectors, | ||
]; | ||
})()); | ||
}; | ||
// shortcut rules: | ||
/** | ||
* Defines component's variants. | ||
* @returns A `StyleCollection` represents the component's variants. | ||
* @returns A `Rule` represents the component's variants. | ||
*/ | ||
export const variants = (variants, options = defaultRuleOptions) => rules(variants, options); | ||
export const variants = (variants, options = defaultSelectorOptions) => rules(variants, options); | ||
const defaultStateOptions = { | ||
...defaultSelectorOptions, | ||
minSpecificityWeight: 3, | ||
inherit: false, | ||
}; | ||
/** | ||
* Defines component's states. | ||
* @param inherit `true` to inherit states from parent element -or- `false` to create independent states. | ||
* @returns A `StyleCollection` represents the component's states. | ||
* @returns A `Rule` represents the component's states. | ||
*/ | ||
export const states = (states, inherit = false, options = { ...defaultRuleOptions, minSpecificityWeight: 3 }) => { | ||
export const states = (states, options = defaultStateOptions) => { | ||
const { inherit = defaultStateOptions.inherit, } = options; | ||
return rules((typeof (states) === 'function') ? states(inherit) : states, options); | ||
}; | ||
// rule items: | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `RuleEntry` represents the component's rule. | ||
*/ | ||
export const rule = (selectors, styles) => [selectors, styles]; | ||
// shortcut rule items: | ||
export const noRule = (styles) => rule('&', styles); | ||
// rule shortcuts: | ||
export const keyframes = (name, items) => rule(`@keyframes ${name}`, Object.fromEntries(Object.entries(items).map(([key, frame]) => [Symbol(key), frame]))); | ||
export const noRule = (...styles) => rule('&', styles); | ||
export const emptyRule = () => rule(null, null); | ||
export const atRoot = (styles) => rule(':root', styles); | ||
export const atGlobal = (styles) => rule('@global', styles); | ||
export const fontFace = (styles) => atGlobal(rules([ | ||
rule('@font-face', styles), | ||
])); | ||
export const isFirstChild = (styles) => rule(':first-child', styles); | ||
export const isNotFirstChild = (styles) => rule(':not(:first-child)', styles); | ||
export const isLastChild = (styles) => rule(':last-child', styles); | ||
export const isNotLastChild = (styles) => rule(':not(:last-child)', styles); | ||
export const isNthChild = (step, offset, styles) => { | ||
export const fallbacks = (...styles) => rule('@fallbacks', styles); | ||
export const fontFace = (...styles) => rule('@font-face', styles); | ||
export const atGlobal = (...rules) => rule('@global', rules); | ||
export const atRoot = (...styles) => rule(':root', styles); | ||
export const isFirstChild = (...styles) => rule(':first-child', styles); | ||
export const isNotFirstChild = (...styles) => rule(':not(:first-child)', styles); | ||
export const isLastChild = (...styles) => rule(':last-child', styles); | ||
export const isNotLastChild = (...styles) => rule(':not(:last-child)', styles); | ||
export const isNthChild = (step, offset, ...styles) => { | ||
if (step === 0) { // no step | ||
if (offset === 0) | ||
return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
return emptyRule(); // 0th => never => return empty rule | ||
if (offset === 1) | ||
return isFirstChild(styles); | ||
return rule(`:nth-child(${offset})`, styles); | ||
return isFirstChild(styles); // 1st | ||
return rule(`:nth-child(${offset})`, styles); // 2nd, 3rd, 4th, ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) | ||
return rule(`:nth-child(n)`, styles); // always match | ||
return rule(`:nth-child(n+${offset})`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) | ||
return rule(`:nth-child(${step}n)`, styles); | ||
return rule(`:nth-child(${step}n+${offset})`, styles); | ||
} // if | ||
}; | ||
export const isNotNthChild = (step, offset, styles) => { | ||
export const isNotNthChild = (step, offset, ...styles) => { | ||
if (step === 0) { // no step | ||
// if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) | ||
return isNthChild(1, 0, styles); // not 0th => not never => always match | ||
if (offset === 1) | ||
return isNotFirstChild(styles); | ||
return rule(`:not(:nth-child(${offset}))`, styles); | ||
return isNotFirstChild(styles); // not 1st | ||
return rule(`:not(:nth-child(${offset}))`, styles); // not 2nd, not 3rd, not 4th, not ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) | ||
return emptyRule(); // never match | ||
return rule(`:not(:nth-child(n+${offset}))`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) | ||
return rule(`:not(:nth-child(${step}n))`, styles); | ||
return rule(`:not(:nth-child(${step}n+${offset}))`, styles); | ||
} // if | ||
}; | ||
export const isNthLastChild = (step, offset, styles) => { | ||
export const isNthLastChild = (step, offset, ...styles) => { | ||
if (step === 0) { // no step | ||
if (offset === 0) | ||
return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
return emptyRule(); // 0th => never => return empty rule | ||
if (offset === 1) | ||
return isLastChild(styles); | ||
return rule(`:nth-last-child(${offset})`, styles); | ||
return isLastChild(styles); // 1st | ||
return rule(`:nth-last-child(${offset})`, styles); // 2nd, 3rd, 4th, ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) | ||
return rule(`:nth-last-child(n)`, styles); // always match | ||
return rule(`:nth-last-child(n+${offset})`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) | ||
return rule(`:nth-last-child(${step}n)`, styles); | ||
return rule(`:nth-last-child(${step}n+${offset})`, styles); | ||
} // if | ||
}; | ||
export const isNotNthLastChild = (step, offset, styles) => { | ||
export const isNotNthLastChild = (step, offset, ...styles) => { | ||
if (step === 0) { // no step | ||
// if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) | ||
return isNthChild(1, 0, styles); // not 0th last => not never => always match | ||
if (offset === 1) | ||
return isNotLastChild(styles); | ||
return rule(`:not(:nth-last-child(${offset}))`, styles); | ||
return isNotLastChild(styles); // not 1st last | ||
return rule(`:not(:nth-last-child(${offset}))`, styles); // not 2nd last, not 3rd last, not 4th last, not ... last | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) | ||
return emptyRule(); // never match | ||
return rule(`:not(:nth-last-child(n+${offset}))`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) | ||
return rule(`:not(:nth-last-child(${step}n))`, styles); | ||
return rule(`:not(:nth-last-child(${step}n+${offset}))`, styles); | ||
} // if | ||
}; | ||
export const isActive = (styles) => rule(':active', styles); | ||
export const isNotActive = (styles) => rule(':not(:active)', styles); | ||
export const isFocus = (styles) => rule(':focus', styles); | ||
export const isNotFocus = (styles) => rule(':not(:focus)', styles); | ||
export const isFocusVisible = (styles) => rule(':focus-visible', styles); | ||
export const isNotFocusVisible = (styles) => rule(':not(:focus-visible)', styles); | ||
export const isHover = (styles) => rule(':hover', styles); | ||
export const isNotHover = (styles) => rule(':not(:hover)', styles); | ||
export const isEmpty = (styles) => rule(':empty', styles); | ||
export const isNotEmpty = (styles) => rule(':not(:empty)', styles); | ||
export const isActive = (...styles) => rule(':active', styles); | ||
export const isNotActive = (...styles) => rule(':not(:active)', styles); | ||
export const isFocus = (...styles) => rule(':focus', styles); | ||
export const isNotFocus = (...styles) => rule(':not(:focus)', styles); | ||
export const isFocusVisible = (...styles) => rule(':focus-visible', styles); | ||
export const isNotFocusVisible = (...styles) => rule(':not(:focus-visible)', styles); | ||
export const isHover = (...styles) => rule(':hover', styles); | ||
export const isNotHover = (...styles) => rule(':not(:hover)', styles); | ||
export const isEmpty = (...styles) => rule(':empty', styles); | ||
export const isNotEmpty = (...styles) => rule(':not(:empty)', styles); | ||
//combinators: | ||
export const combinators = (combinator, selectors, styles, options = defaultSelectorOptions) => { | ||
const combiSelectors = flat(selectors).filter((selector) => !!selector).map((selector) => { | ||
// if (selector === '&') return selector; // no children => the parent itself | ||
if (selector.includes('&')) | ||
return selector; // custom combinator | ||
if (((combinator === ' ') || (combinator === '>')) && selector.startsWith('::')) | ||
return `&${selector}`; // pseudo element => attach the parent itself (for descendants & children) | ||
return `&${combinator}${selector}`; | ||
}); | ||
if (!combiSelectors.length) | ||
return {}; // no selector => return empty | ||
return rule(combiSelectors, styles, options); | ||
}; | ||
export const descendants = (selectors, styles, options = defaultSelectorOptions) => combinators(' ', selectors, styles, options); | ||
export const children = (selectors, styles, options = defaultSelectorOptions) => combinators('>', selectors, styles, options); | ||
export const siblings = (selectors, styles, options = defaultSelectorOptions) => combinators('~', selectors, styles, options); | ||
export const nextSiblings = (selectors, styles, options = defaultSelectorOptions) => combinators('+', selectors, styles, options); | ||
// utilities: | ||
@@ -555,14 +816,3 @@ /** | ||
} // if | ||
const merged = []; | ||
for (const item of collection) { | ||
if (Array.isArray(item)) { | ||
// an array => DeepArray<T> => recursively `flat()` | ||
merged.push(...flat(item)); | ||
} | ||
else { | ||
// not an array => T | ||
merged.push(item); | ||
} // if | ||
} // for | ||
return merged; | ||
return collection.flat(Infinity); | ||
}; | ||
@@ -569,0 +819,0 @@ export const iif = (condition, content) => { |
{ | ||
"name": "@cssfn/cssfn", | ||
"version": "1.0.8", | ||
"version": "1.0.9", | ||
"description": "Writes CSS in javascript function.", | ||
@@ -31,11 +31,11 @@ "type": "module", | ||
"dependencies": { | ||
"@cssfn/css-types": "^1.0.2", | ||
"@cssfn/jss-plugin-camel-case": "^1.0.0", | ||
"@cssfn/jss-plugin-extend": "^1.0.1", | ||
"@cssfn/jss-plugin-global": "^1.0.3", | ||
"@cssfn/jss-plugin-nested": "^1.0.1", | ||
"@cssfn/jss-plugin-short": "^1.0.2", | ||
"@cssfn/jss-plugin-vendor": "^1.0.0", | ||
"@cssfn/types": "^1.0.3", | ||
"@cssfn/css-selector": "^1.0.4", | ||
"@cssfn/css-types": "^1.0.4", | ||
"@cssfn/jss-plugin-camel-case": "^1.0.1", | ||
"@cssfn/jss-plugin-nested": "^1.0.2", | ||
"@cssfn/jss-plugin-short": "^1.0.3", | ||
"@cssfn/jss-plugin-vendor": "^1.0.1", | ||
"@cssfn/types": "^1.0.4", | ||
"camel-case": "^4.1.2", | ||
"csstype": "^3.0.10", | ||
"jss": "^10.9.0", | ||
@@ -42,0 +42,0 @@ "pascal-case": "^3.1.2", |
1373
src/cssfn.ts
// jss: | ||
import { | ||
// general types: | ||
JssStyle, | ||
JssValue, | ||
Classes, | ||
@@ -16,8 +14,2 @@ Styles, | ||
// custom jss-plugins: | ||
import jssPluginGlobal from '@cssfn/jss-plugin-global' | ||
import { | ||
default as jssPluginExtend, | ||
ExtendableStyle, | ||
mergeStyle, | ||
} from '@cssfn/jss-plugin-extend' | ||
import jssPluginNested from '@cssfn/jss-plugin-nested' | ||
@@ -31,3 +23,2 @@ import jssPluginShort from '@cssfn/jss-plugin-short' | ||
OptionalOrFalse, | ||
SingleOrArray, | ||
SingleOrDeepArray, | ||
@@ -46,4 +37,55 @@ ProductOrFactoryOrDeepArray, | ||
} from '@cssfn/css-types' // ts defs support for cssfn | ||
import { | ||
// types: | ||
SimpleSelector as SimpleSelectorModel, | ||
Combinator, | ||
Selector as SelectorModel, | ||
SelectorList as SelectorModelList, | ||
PureSelector as PureSelectorModel, | ||
PureSelectorList as PureSelectorModelList, | ||
// parses: | ||
parseSelectors, | ||
// creates & tests: | ||
parentSelector, | ||
pseudoClassSelector, | ||
isSimpleSelector, | ||
isParentSelector, | ||
isClassOrPseudoClassSelector, | ||
isPseudoElementSelector, | ||
isNotPseudoElementSelector, | ||
isCombinator, | ||
createSelector, | ||
createSelectorList, | ||
isNotEmptySelectorEntry, | ||
isNotEmptySelector, | ||
isNotEmptySelectors, | ||
// renders: | ||
selectorsToString, | ||
// transforms: | ||
groupSelectors, | ||
groupSelector, | ||
ungroupSelector, | ||
// measures: | ||
calculateSpecificity, | ||
} from '@cssfn/css-selector' | ||
// others libs: | ||
import { | ||
Properties as CssProperties, | ||
} from 'csstype' | ||
import { pascalCase } from 'pascal-case' // pascal-case support for jss | ||
@@ -57,45 +99,43 @@ import { camelCase } from 'camel-case' // camel-case support for jss | ||
export type { JssStyle, JssValue, Classes, Styles, StyleSheet } | ||
export type { Classes, Styles, StyleSheet } | ||
export type { Prop, PropEx, Cust } | ||
export type { Dictionary, ValueOf, DictionaryOf } | ||
export type Style = (ExtendableStyle & {}) | ||
export type StyleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Style>> | ||
export type KnownCssPropName = keyof CssProperties<string|number> | ||
export type KnownCssPropValue | ||
<PropName extends KnownCssPropName> = Exclude<CssProperties<string|number>[PropName], (undefined|null)> | ||
// comment docs disappears in TypeScript: | ||
// export type KnownCssProps = { [PropName in KnownCssPropName] ?: (KnownCssPropValue<PropName>|[[KnownCssPropValue<PropName>], '!important']|CssValue) } | ||
// comment docs preserves in TypeScript: | ||
export type KnownCssProps = { [PropName in keyof CssProperties<string|number>] ?: (KnownCssPropValue<PropName>|[[KnownCssPropValue<PropName>], '!important']|CssValue) } | ||
export type ClassName = string // not a really string: [A-Z_a-z-]+ | ||
export type RealClass = (`.${ClassName}` & {}) | ||
export type PseudoClass = (`:${ClassName}` & {}) | ||
export type Class = RealClass|PseudoClass | ||
export type ClassEntry<TClassName extends ClassName = ClassName> = readonly [TClassName, StyleCollection] | ||
export type ClassList <TClassName extends ClassName = ClassName> = ClassEntry<TClassName>[] | ||
export type BasicCssValue = (string & {}) | (number & {}) | PropEx.Keyframes | ||
export type CssValue = undefined | null | BasicCssValue | BasicCssValue[] | (BasicCssValue|BasicCssValue[]|'!important')[] | ||
export type OptionalString = OptionalOrFalse<string> | ||
export type CustomCssProps = { [PropName: Exclude<string, KnownCssPropName>] : CssValue } | ||
export type UniversalSelector = ('*' & {}) | ||
export type RealElementSelector = (string & {}) // not a really string: [A-Z_a-z-]+ | ||
export type PseudoElementSelector = (`::${string}` & {}) // not a really string: [A-Z_a-z-]+ | ||
export type ElementSelector = RealElementSelector|PseudoElementSelector | ||
export type ClassSelector = (Class & {}) | ||
export type IdSelector = (`#${string}` & {}) | ||
export type SingleSelector = UniversalSelector|ElementSelector|ClassSelector|IdSelector | ||
export type Selector = | SingleSelector | ||
| (`${SingleSelector}${SingleSelector}` & {}) | ||
| (`${SingleSelector}${SingleSelector}${SingleSelector}` & {}) | ||
| (`${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}` & {}) | ||
| (`${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}${SingleSelector}` & {}) | ||
export type SelectorCollection = SingleOrDeepArray<OptionalOrFalse<Selector>> | ||
export type CssProps = KnownCssProps & CustomCssProps | ||
export type Rule = { [PropName: symbol] : StyleCollection } | ||
export type NestedSelector = | '&' | ||
| (`&${Selector}` & {}) | ||
| (`${Selector}&` & {}) | ||
export type Style = CssProps & Rule | ||
export type StyleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Style>> | ||
export type RuleEntry = readonly [SelectorCollection, StyleCollection] | ||
export type RuleEntrySource = ProductOrFactory<OptionalOrFalse<RuleEntry>> | ||
export type RuleList = RuleEntrySource[] | ||
export type RuleCollection = SingleOrArray<RuleEntrySource|RuleList> | ||
export type ClassName = string // not a really string: [A-Z_a-z-]+ | ||
export type RealClass = (`.${ClassName}` & {}) | ||
export type PseudoClass = (`:${ClassName}` & {}) | ||
export type Class = RealClass|PseudoClass | ||
export type ClassEntry | ||
<TClassName extends ClassName = ClassName> = readonly [TClassName, StyleCollection] | ||
export type ClassList | ||
<TClassName extends ClassName = ClassName> = ClassEntry<TClassName>[] | ||
export type PropList = Dictionary<JssValue> | ||
export type OptionalString = OptionalOrFalse<string> | ||
export type Selector = (string & {}) | ||
export type SelectorCollection = SingleOrDeepArray<OptionalOrFalse<Selector>> | ||
export type RuleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Rule>> | ||
// utilities: | ||
@@ -164,5 +204,3 @@ | ||
const customJss = createJss().setup({createGenerateId, plugins:[ | ||
jssPluginGlobal(), // requires to be placed before all other plugins | ||
jssPluginExtend(), | ||
jssPluginNested(), | ||
jssPluginNested((styles) => mergeStyles(styles as StyleCollection) as {}), | ||
jssPluginShort(), // requires to be placed before `camelCase` | ||
@@ -207,4 +245,4 @@ jssPluginCamelCase(), | ||
*/ | ||
.map(([className, styles]): Style => ({ [className || '@global'] : mergeStyles(styles) })) // convert each `[className, styles]` to `{ className : mergeStyles(styles) | null }` | ||
) ?? {}) as Styles<TClassName>; | ||
.map(([className, styles]): Style => ({ [className || '@global'] : mergeStyles(styles) as CssValue })) // convert each `[className, styles]` to `{ className : mergeStyles(styles) | null }` | ||
) ?? emptyMergedStyle) as Styles<TClassName>; | ||
} | ||
@@ -214,9 +252,115 @@ | ||
// compositions: | ||
// processors: | ||
const isStyle = (object: any): object is Style => object && (typeof(object) === 'object') && !Array.isArray(object); | ||
const mergeLiteral = (style: Style, newStyle: Style): void => { | ||
for (const [propName, newPropValue] of [ | ||
...Object.entries(newStyle), | ||
...Object.getOwnPropertySymbols(newStyle).map((sym) => [sym, newStyle[sym]] as const), | ||
]) { // loop through `newStyle`'s props | ||
if (!isStyle(newPropValue)) { | ||
// `newPropValue` is not a `Style` => unmergeable => add/overwrite `newPropValue` into `style`: | ||
delete style[propName]; // delete the old prop (if any), so the new prop always placed at the end of LiteralObject | ||
style[propName] = newPropValue as any; // add/overwrite | ||
} | ||
else { | ||
// `newPropValue` is a `Style` => possibility to merge with `currentPropValue` | ||
const currentPropValue = style[propName]; | ||
if (!isStyle(currentPropValue)) { | ||
// `currentPropValue` is not a `Style` => unmergeable => add/overwrite `newPropValue` into `style`: | ||
delete style[propName]; // delete the old prop (if any), so the new prop always placed at the end of LiteralObject | ||
style[propName] = newPropValue as any; // add/overwrite | ||
} | ||
else { | ||
// both `newPropValue` & `currentPropValue` are `Style` => merge them recursively (deeply): | ||
const currentValueClone = {...currentPropValue} as Style; // clone the `currentPropValue` to avoid side effect, because the `currentPropValue` is not **the primary object** we're working on | ||
mergeLiteral(currentValueClone, newPropValue); | ||
// merging style prop no need to rearrange the prop position | ||
style[propName] = currentValueClone as any; // set the mutated `currentValueClone` back to `style` | ||
} // if | ||
} // if | ||
} // for | ||
} | ||
const mergeNested = (style: Style): Style => { | ||
//#region group (nested) Rule(s) by selector name | ||
const groupByNested = ( | ||
Object.getOwnPropertySymbols(style) | ||
.reduce((accum, sym) => { | ||
const nestedSelector = sym.description ?? ''; | ||
if ( | ||
// nested rules: | ||
nestedSelector.includes('&') | ||
|| | ||
// conditional rules & globals: | ||
['@media', '@supports', '@document', '@global'].some((at) => nestedSelector.startsWith(at)) | ||
) { | ||
let group = accum.get(nestedSelector); // get an existing collector | ||
if (!group) accum.set(nestedSelector, group = []); // create a new collector | ||
group.push(sym); | ||
} // if | ||
return accum; | ||
}, new Map<string, symbol[]>())); | ||
//#endregion group (nested) Rule(s) by selector name | ||
//#region merge duplicates (nested) Rule(s) to unique ones | ||
for (const group of Array.from(groupByNested.values())) { | ||
if (group.length <= 1) continue; // filter out groups with single/no member | ||
const mergedStyles = mergeStyles( | ||
group.map((sym) => style[sym]) | ||
); | ||
if (mergedStyles) { | ||
// update last member | ||
style[group[group.length - 1]] = mergedStyles; // merge all member's style to the last member | ||
} | ||
else { | ||
// mergedStyles is empty => delete last member | ||
delete style[group[group.length - 1]]; | ||
} // if | ||
for (const sym of group.slice(0, -1)) delete style[sym]; // delete first member to second last member | ||
} // for | ||
//#endregion merge duplicates (nested) Rule to unique ones | ||
//#region merge only_parentSelector into current style | ||
const parentSelector = groupByNested.get('&')?.pop(); // remove & get the last member in parentSelector group | ||
if (parentSelector) { | ||
const parentStyles = style[parentSelector]; | ||
const mergedParentStyles = mergeStyles(parentStyles); | ||
if (mergedParentStyles) { | ||
mergeLiteral(style, mergedParentStyles); // merge into current style | ||
delete style[parentSelector]; // merged => delete source | ||
} // if | ||
} // if | ||
//#endregion merge only_parentSelector into current style | ||
return style; | ||
} | ||
// prevents JSS to clone the CSSFN Style (because the symbol props are not copied) | ||
class MergedStyle { | ||
constructor(style?: Style) { | ||
if (style) Object.assign(this, style); | ||
} | ||
}; | ||
const emptyMergedStyle = new MergedStyle(); | ||
Object.seal(emptyMergedStyle); | ||
/** | ||
* Defines the (sub) component's composition. | ||
* @returns A `StyleCollection` represents the (sub) component's composition. | ||
*/ | ||
export const composition = (styles: StyleCollection[]): StyleCollection => styles; | ||
/** | ||
* Merges the (sub) component's composition to single `Style`. | ||
@@ -227,3 +371,3 @@ * @returns A `Style` represents the merged (sub) component's composition | ||
*/ | ||
export const mergeStyles = (styles: StyleCollection): Style|null => { | ||
export const mergeStyles = (styles: StyleCollection): Style|null => { | ||
/* | ||
@@ -251,3 +395,12 @@ StyleCollection = ProductOrFactoryOrDeepArray<OptionalOrFalse<Style>> | ||
return styleValue; | ||
const mergedStyles: Style = (new MergedStyle(styleValue) as Style); | ||
mergeNested(mergedStyles); | ||
// do not return an empty style, instead return null: | ||
if ((!Object.keys(mergedStyles).length) && (!Object.getOwnPropertySymbols(mergedStyles).length)) return null; // an empty object => return `null` | ||
// return non empty style: | ||
return mergedStyles; | ||
} // if | ||
@@ -257,9 +410,11 @@ | ||
const mergedStyles: Style = {} | ||
for (const subStyles of styles) { | ||
const mergedStyles: Style = (new MergedStyle() as Style); | ||
for (const subStyles of styles) { // shallow iterating array | ||
const subStyleValue: OptionalOrFalse<Style> = ( | ||
Array.isArray(subStyles) | ||
? | ||
// deep iterating array | ||
mergeStyles(subStyles) // an array => ProductOrFactoryDeepArray<OptionalOrFalse<Style>> => recursively `mergeStyles()` | ||
: | ||
// final element => might be a function or a product | ||
( | ||
@@ -279,7 +434,490 @@ // not an array => ProductOrFactory<OptionalOrFalse<Style>> | ||
mergeStyle(mergedStyles, subStyleValue); | ||
// merge current style to single big style (string props + symbol props): | ||
mergeLiteral(mergedStyles, subStyleValue); | ||
} // for | ||
if (Object.keys(mergedStyles).length === 0) return null; // an empty object => return `null` | ||
mergeNested(mergedStyles); | ||
// do not return an empty style, instead return null: | ||
if ((!Object.keys(mergedStyles).length) && (!Object.getOwnPropertySymbols(mergedStyles).length)) return null; // an empty object => return `null` | ||
// return non empty style: | ||
return mergedStyles; | ||
} | ||
const nthChildNSelector = pseudoClassSelector('nth-child', 'n'); | ||
const adjustSpecificityWeight = (selectorList: PureSelectorModelList, minSpecificityWeight: number|null, maxSpecificityWeight: number|null): PureSelectorModelList => { | ||
if ( | ||
(minSpecificityWeight == null) | ||
&& | ||
(maxSpecificityWeight == null) | ||
) return selectorList; // nothing to adjust | ||
//#region group selectors by specificity weight status | ||
const enum SpecificityWeightStatus { | ||
Fit, | ||
TooBig, | ||
TooSmall, | ||
} | ||
type GroupBySpecificityWeightStatus = Map<SpecificityWeightStatus, { selector: PureSelectorModel, specificityWeight: number }[]> | ||
const selectorListBySpecificityWeightStatus = selectorList.map((selector) => selector.filter(isNotEmptySelectorEntry) as PureSelectorModel).reduce( | ||
(accum, selector): GroupBySpecificityWeightStatus => { | ||
const [specificityWeight, weightStatus] = ((): readonly [number, SpecificityWeightStatus] => { | ||
const specificityWeight = calculateSpecificity(selector)[1]; | ||
if ((maxSpecificityWeight !== null) && (specificityWeight > maxSpecificityWeight)) { | ||
return [specificityWeight, SpecificityWeightStatus.TooBig]; | ||
} // if | ||
if ((minSpecificityWeight !== null) && (specificityWeight < minSpecificityWeight)) { | ||
return [specificityWeight, SpecificityWeightStatus.TooSmall]; | ||
} // if | ||
return [specificityWeight, SpecificityWeightStatus.Fit]; | ||
})(); | ||
let group = accum.get(weightStatus); // get an existing collector | ||
if (!group) accum.set(weightStatus, group = []); // create a new collector | ||
group.push({ selector, specificityWeight }); | ||
return accum; | ||
}, | ||
new Map<SpecificityWeightStatus, { selector: PureSelectorModel, specificityWeight: number }[]>() | ||
); | ||
//#endregion group selectors by specificity weight status | ||
const fitSelectors = selectorListBySpecificityWeightStatus.get(SpecificityWeightStatus.Fit ) ?? []; | ||
const tooBigSelectors = selectorListBySpecificityWeightStatus.get(SpecificityWeightStatus.TooBig ) ?? []; | ||
const tooSmallSelectors = selectorListBySpecificityWeightStatus.get(SpecificityWeightStatus.TooSmall ) ?? []; | ||
return createSelectorList( | ||
...fitSelectors.map((group) => group.selector), | ||
...tooBigSelectors.flatMap((group) => { | ||
const reversedSelector = group.selector.reverse(); // reverse & mutate the current `group.selector` array | ||
type SelectorAccum = { remaining: number, reducedSelector: SelectorModel } | ||
const { reducedSelector: reversedReducedSelector, remaining: remainingSpecificityWeight } : SelectorAccum = ( | ||
reversedSelector.slice(0) // clone the `reversedSelector` because the `reduce()` uses `splice()` to break the iteration | ||
.reduce((accum, selectorEntry, index, array): SelectorAccum => { | ||
if (accum.remaining <= 0) { | ||
array.splice(1); // eject early by mutating iterated copy - it's okay to **mutate** the `array` because it already cloned at `slice(0)` | ||
return accum; | ||
} // if | ||
if (isSimpleSelector(selectorEntry)) { | ||
const [ | ||
/* | ||
selector tokens: | ||
'&' = parent selector | ||
'*' = universal selector | ||
'[' = attribute selector | ||
'' = element selector | ||
'#' = ID selector | ||
'.' = class selector | ||
':' = pseudo class selector | ||
'::' = pseudo element selector | ||
*/ | ||
selectorToken, | ||
/* | ||
selector name: | ||
string = the name of [element, ID, class, pseudo class, pseudo element] selector | ||
*/ | ||
selectorName, | ||
/* | ||
selector parameter(s): | ||
string = the parameter of pseudo class selector, eg: nth-child(2n+3) => '2n+3' | ||
array = [name, operator, value, options] of attribute selector, eg: [data-msg*="you & me" i] => ['data-msg', '*=', 'you & me', 'i'] | ||
SelectorList = nested selector(s) of pseudo class [:is(...), :where(...), :not(...)] | ||
*/ | ||
// selectorParams, | ||
] = selectorEntry; | ||
if (selectorToken === ':') { | ||
switch (selectorName) { | ||
case 'is': | ||
case 'not': | ||
case 'has': | ||
const specificityWeight = calculateSpecificity([selectorEntry])[1]; | ||
accum.remaining -= specificityWeight; // reduce the counter | ||
break; | ||
case 'where': | ||
break; // don't reduce the counter | ||
default: | ||
accum.remaining--; // reduce the counter | ||
} // switch | ||
} | ||
else if (['.', '[',].includes(selectorToken)) { | ||
accum.remaining--; // reduce the counter | ||
} // if | ||
} // if | ||
accum.reducedSelector.push(selectorEntry); | ||
return accum; | ||
}, ({ | ||
remaining : (group.specificityWeight - (maxSpecificityWeight ?? group.specificityWeight)), | ||
reducedSelector : [], | ||
} as SelectorAccum)) | ||
); | ||
const [whereSelector, ...pseudoElmSelectors] = groupSelector( | ||
reversedReducedSelector.reverse(), | ||
{ selectorName: 'where' } | ||
); | ||
whereSelector.unshift( | ||
...reversedSelector.slice(reversedReducedSelector.length).reverse(), | ||
); | ||
whereSelector.push( | ||
...(new Array<SimpleSelectorModel>((remainingSpecificityWeight < 0) ? -remainingSpecificityWeight : 0)).fill( | ||
nthChildNSelector // or use `nth-child(n)` | ||
), | ||
); | ||
return createSelectorList( | ||
whereSelector, | ||
...pseudoElmSelectors, | ||
); | ||
}), | ||
...tooSmallSelectors.map((group) => createSelector( | ||
...group.selector, | ||
...(new Array<SimpleSelectorModel>((minSpecificityWeight ?? 1) - group.specificityWeight)).fill( | ||
group.selector | ||
.filter(isClassOrPseudoClassSelector) // only interested to class selector -or- pseudo class selector | ||
.filter((simpleSelector) => { // pseudo class selector without parameters | ||
const [ | ||
/* | ||
selector tokens: | ||
'&' = parent selector | ||
'*' = universal selector | ||
'[' = attribute selector | ||
'' = element selector | ||
'#' = ID selector | ||
'.' = class selector | ||
':' = pseudo class selector | ||
'::' = pseudo element selector | ||
*/ | ||
// selectorToken | ||
, | ||
/* | ||
selector name: | ||
string = the name of [element, ID, class, pseudo class, pseudo element] selector | ||
*/ | ||
// selectorName | ||
, | ||
/* | ||
selector parameter(s): | ||
string = the parameter of pseudo class selector, eg: nth-child(2n+3) => '2n+3' | ||
array = [name, operator, value, options] of attribute selector, eg: [data-msg*="you & me" i] => ['data-msg', '*=', 'you & me', 'i'] | ||
SelectorList = nested selector(s) of pseudo class [:is(...), :where(...), :not(...)] | ||
*/ | ||
selectorParams, | ||
] = simpleSelector; | ||
return (selectorParams === undefined); | ||
}) | ||
.pop() // repeats the last selector until minSpecificityWeight satisfied | ||
?? | ||
nthChildNSelector // or use `nth-child(n)` | ||
) | ||
)), | ||
); | ||
}; | ||
export interface SelectorOptions { | ||
groupSelectors ?: boolean | ||
specificityWeight ?: number|null | ||
minSpecificityWeight ?: number|null | ||
maxSpecificityWeight ?: number|null | ||
} | ||
const defaultSelectorOptions : Required<SelectorOptions> = { | ||
groupSelectors : true, | ||
specificityWeight : null, | ||
minSpecificityWeight : null, | ||
maxSpecificityWeight : null, | ||
}; | ||
export const mergeSelectors = (selectorList: SelectorModelList, options: SelectorOptions = defaultSelectorOptions): SelectorModelList => { | ||
const { | ||
groupSelectors : doGroupSelectors = defaultSelectorOptions.groupSelectors, | ||
specificityWeight, | ||
} = options; | ||
const minSpecificityWeight = specificityWeight ?? options.minSpecificityWeight ?? null; | ||
const maxSpecificityWeight = specificityWeight ?? options.maxSpecificityWeight ?? null; | ||
if ( | ||
!doGroupSelectors // do not perform grouping | ||
&& | ||
(minSpecificityWeight === null) && (maxSpecificityWeight === null) // do not perform transform | ||
) return selectorList; // nothing to do | ||
const normalizedSelectorList = ( | ||
selectorList | ||
.flatMap((selector) => ungroupSelector(selector)) | ||
.filter(isNotEmptySelector) | ||
); | ||
if ( | ||
(!doGroupSelectors || (normalizedSelectorList.length <= 1)) // do not perform grouping || only singular => nothing to group | ||
&& | ||
(minSpecificityWeight === null) && (maxSpecificityWeight === null) // do not perform transform | ||
) return normalizedSelectorList; // nothing to do | ||
// transform: | ||
const adjustedSelectorList = adjustSpecificityWeight( | ||
normalizedSelectorList | ||
, | ||
minSpecificityWeight, | ||
maxSpecificityWeight | ||
); | ||
if ( | ||
(!doGroupSelectors || (adjustedSelectorList.length <= 1)) // do not perform grouping || only singular => nothing to group | ||
) return adjustedSelectorList; // nothing to do | ||
//#region group selectors by parent position | ||
const enum ParentPosition { | ||
OnlyParent, | ||
OnlyBeginParent, | ||
OnlyEndParent, | ||
RandomParent, | ||
} | ||
type GroupByParentPosition = Map<ParentPosition, PureSelectorModel[]> | ||
const selectorListByParentPosition = adjustedSelectorList.map((selector) => selector.filter(isNotEmptySelectorEntry) as PureSelectorModel).reduce( | ||
(accum, selector): GroupByParentPosition => { | ||
const position = ((): ParentPosition => { | ||
const hasFirstParent = ((): boolean => { | ||
if (selector.length < 1) return false; // at least 1 entry must exist, for the first_parent | ||
const firstSelectorEntry = selector[0]; // take the first entry | ||
return isParentSelector(firstSelectorEntry); // the entry must be ParentSelector | ||
})(); | ||
const onlyParent = hasFirstParent && (selector.length === 1); | ||
if (onlyParent) return ParentPosition.OnlyParent; | ||
const hasMiddleParent = ((): boolean => { | ||
if (selector.length < 3) return false; // at least 3 entry must exist, the first & last are already reserved, the middle one is the middle_parent | ||
for (let index = 1, maxIndex = (selector.length - 2); index <= maxIndex; index++) { | ||
const middleSelectorEntry = selector[index]; // take the 2nd_first_entry until the 2nd_last_entry | ||
if (isParentSelector(middleSelectorEntry)) return true; // the entry must be ParentSelector, otherwise skip to next | ||
} // for | ||
return false; // ran out of iterator => not found | ||
})(); | ||
const hasLastParent = ((): boolean => { | ||
const length = selector.length; | ||
if (length < 2) return false; // at least 2 entry must exist, the first is already reserved, the last one is the last_parent | ||
const lastSelectorEntry = selector[length - 1]; // take the last entry | ||
return isParentSelector(lastSelectorEntry); // the entry must be ParentSelector | ||
})(); | ||
const onlyBeginParent = hasFirstParent && !hasMiddleParent && !hasLastParent; | ||
if (onlyBeginParent) return ParentPosition.OnlyBeginParent; | ||
const onlyEndParent = !hasFirstParent && !hasMiddleParent && hasLastParent; | ||
if (onlyEndParent) return ParentPosition.OnlyEndParent; | ||
return ParentPosition.RandomParent; | ||
})(); | ||
let group = accum.get(position); // get an existing collector | ||
if (!group) accum.set(position, group = []); // create a new collector | ||
group.push(selector); | ||
return accum; | ||
}, | ||
new Map<ParentPosition, PureSelectorModel[]>() | ||
); | ||
//#endregion group selectors by parent position | ||
const onlyParentSelectorList = selectorListByParentPosition.get(ParentPosition.OnlyParent ) ?? []; | ||
const onlyBeginParentSelectorList = selectorListByParentPosition.get(ParentPosition.OnlyBeginParent ) ?? []; | ||
const onlyEndParentSelectorList = selectorListByParentPosition.get(ParentPosition.OnlyEndParent ) ?? []; | ||
const randomParentSelectorList = selectorListByParentPosition.get(ParentPosition.RandomParent ) ?? []; | ||
type GroupByCombinator = Map<Combinator|null, PureSelectorModelList> | ||
const createGroupByCombinator = (fetch: (selector: PureSelectorModel) => Combinator|null) => (accum: GroupByCombinator, selector: PureSelectorModel): GroupByCombinator => { | ||
const combinator = fetch(selector); | ||
let group = accum.get(combinator); // get an existing collector | ||
if (!group) accum.set(combinator, group = []); // create a new collector | ||
group.push(selector); | ||
return accum; | ||
}; | ||
const groupedSelectorList = createSelectorList( | ||
// only ParentSelector | ||
// & | ||
!!onlyParentSelectorList.length && ( | ||
onlyParentSelectorList[0] // just take the first one, the rest are guaranteed to be the same | ||
), | ||
// ParentSelector at beginning | ||
// &aaa | ||
// &:is(aaa, bbb, ccc) | ||
...((): SelectorModelList => { | ||
if (onlyBeginParentSelectorList.length <= 1) return onlyBeginParentSelectorList; // only contain one/no Selector, no need to group | ||
//#region group selectors by combinator | ||
const selectorListByCombinator = onlyBeginParentSelectorList.reduce( | ||
createGroupByCombinator((selector) => { | ||
if (selector.length >= 2) { // at least 2 entry must exist, for the first_parent followed by combinator | ||
const secondSelectorEntry = selector[1]; // take the first_second entry | ||
if (isCombinator(secondSelectorEntry)) { // the entry must be the same as combinator | ||
return secondSelectorEntry; | ||
} // if | ||
} // if | ||
return null; // ungroupable | ||
}), | ||
new Map<Combinator|null, PureSelectorModelList>() | ||
); | ||
//#endregion group selectors by combinator | ||
return Array.from(selectorListByCombinator.entries()).flatMap(([combinator, selectors]) => { | ||
if (selectors.length <= 1) return selectors; // only contain one/no Selector, no need to group | ||
if (selectors.filter((selector) => selector.every(isNotPseudoElementSelector)).length <= 1) return selectors; // only contain one/no Selector without ::pseudo-element, no need to group | ||
const [isSelector, ...pseudoElmSelectors] = groupSelectors( | ||
selectors | ||
.filter(isNotEmptySelector) // remove empty Selector(s) in SelectorList | ||
.map((selector) => selector.slice( | ||
( | ||
combinator | ||
? | ||
2 // remove the first_parent & combinator | ||
: | ||
1 // remove the first_parent | ||
) | ||
+ | ||
(selector.some(isPseudoElementSelector) ? -1 : 0) // exception for ::pseudo-element => do not remove the first_parent | ||
)), | ||
{ selectorName: 'is' } | ||
); | ||
return createSelectorList( | ||
isNotEmptySelector(isSelector) && createSelector( | ||
parentSelector(), // add a ParentSelector before :is(...) | ||
combinator, // add a Combinator (if any) before :is(...) | ||
...isSelector, // :is(...) | ||
), | ||
...pseudoElmSelectors, | ||
); | ||
}); | ||
})(), | ||
// ParentSelector at end | ||
// aaa& | ||
// :is(aaa, bbb, ccc)& | ||
...((): SelectorModelList => { | ||
if (onlyEndParentSelectorList.length <= 1) return onlyEndParentSelectorList; // only contain one/no Selector, no need to group | ||
//#region group selectors by combinator | ||
const selectorListByCombinator = onlyEndParentSelectorList.reduce( | ||
createGroupByCombinator((selector) => { | ||
const length = selector.length; | ||
if (length >= 2) { // at least 2 entry must exist, for the combinator followed by last_parent | ||
const secondSelectorEntry = selector[length - 2]; // take the last_second entry | ||
if (isCombinator(secondSelectorEntry)) { // the entry must be the same as combinator | ||
return secondSelectorEntry; | ||
} // if | ||
} // if | ||
return null; // ungroupable | ||
}), | ||
new Map<Combinator|null, PureSelectorModelList>() | ||
); | ||
//#endregion group selectors by combinator | ||
return Array.from(selectorListByCombinator.entries()).flatMap(([combinator, selectors]) => { | ||
if (selectors.length <= 1) return selectors; // only contain one/no Selector, no need to group | ||
if (selectors.filter((selector) => selector.every(isNotPseudoElementSelector)).length <= 1) return selectors; // only contain one/no Selector without ::pseudo-element, no need to group | ||
const [isSelector, ...pseudoElmSelectors] = groupSelectors( | ||
selectors | ||
.filter(isNotEmptySelector) // remove empty Selector(s) in SelectorList | ||
.map((selector) => selector.slice(0, | ||
( | ||
combinator | ||
? | ||
-2 // remove the combinator & last_parent | ||
: | ||
-1 // remove the last_parent | ||
) | ||
+ | ||
(selector.some(isPseudoElementSelector) ? 1 : 0) // exception for ::pseudo-element => do not remove the last_parent | ||
)), | ||
{ selectorName: 'is' } | ||
); | ||
return createSelectorList( | ||
isNotEmptySelector(isSelector) && createSelector( | ||
...isSelector, // :is(...) | ||
combinator, // add a Combinator (if any) after :is(...) | ||
parentSelector(), // add a ParentSelector after :is(...) | ||
), | ||
...pseudoElmSelectors, | ||
); | ||
}); | ||
})(), | ||
// parent at random | ||
// a&aa, bb&b, c&c&c | ||
...randomParentSelectorList, | ||
); | ||
return groupedSelectorList; | ||
} | ||
// compositions: | ||
/** | ||
@@ -289,3 +927,3 @@ * Defines the additional component's composition. | ||
*/ | ||
export const compositionOf = <TClassName extends ClassName = ClassName>(className: TClassName, styles: StyleCollection[]): ClassEntry<TClassName> => [ | ||
export const compositionOf = <TClassName extends ClassName = ClassName>(className: TClassName, ...styles: StyleCollection[]): ClassEntry<TClassName> => [ | ||
className, | ||
@@ -299,3 +937,3 @@ styles | ||
*/ | ||
export const mainComposition = (styles: StyleCollection[]) => compositionOf('main' , styles); | ||
export const mainComposition = (...styles: StyleCollection[]) => compositionOf('main' , ...styles); | ||
/** | ||
@@ -305,477 +943,291 @@ * Defines the global style applied to a whole document. | ||
*/ | ||
export const globalDef = (ruleCollection: RuleCollection) => compositionOf('' , [rules(ruleCollection)]); | ||
export const imports = (styles: StyleCollection[]) => composition(styles); | ||
export const globalDef = (...rules : RuleCollection[]) => compositionOf('' , ...rules ); | ||
// layouts: | ||
// styles: | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines the (sub) component's composition. | ||
* @returns A `Rule` represents the (sub) component's composition. | ||
*/ | ||
export const composition = (...styles: StyleCollection[]) => noRule(...styles); | ||
/** | ||
* Defines component's style. | ||
* @returns A `Rule` represents the component's style. | ||
*/ | ||
export const style = (style: Style) => noRule(style); | ||
/** | ||
* @deprecated move to `style()` | ||
* Defines component's layout. | ||
* @returns A `Style` represents the component's layout. | ||
* @returns A `Rule` represents the component's layout. | ||
*/ | ||
export const layout = (style: Style): Style => style; | ||
export const layout = (style: Style) => noRule(style); | ||
/** | ||
* Defines component's variable(s). | ||
* @returns A `Style` represents the component's variable(s). | ||
* @returns A `Rule` represents the component's variable(s). | ||
*/ | ||
export const vars = (items: { [name: string]: JssValue }): Style => items; | ||
export const vars = (items: { [key: Cust.Decl]: CssValue }) => noRule(items); | ||
export const imports = (...styles: StyleCollection[]) => noRule(...styles); | ||
//combinators: | ||
export interface CombinatorOptions { | ||
groupSelectors? : boolean | ||
} | ||
const defaultCombinatorOptions : Required<CombinatorOptions> = { | ||
groupSelectors : true, | ||
}; | ||
export const combinators = (combinator: string, selectors: SelectorCollection, styles: StyleCollection, options: CombinatorOptions = defaultCombinatorOptions): PropList => { | ||
const { | ||
groupSelectors = defaultCombinatorOptions.groupSelectors, | ||
} = options; | ||
// rules: | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `Rule` represents the component's rule. | ||
*/ | ||
export const rule = (rules: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions): Rule => { | ||
const rulesString = ( | ||
flat(rules) | ||
.filter((rule): rule is Selector => !!rule) | ||
); | ||
const enum RuleType { | ||
SelectorRule, // &.foo .boo& .foo&.boo | ||
AtRule, // for `@media` | ||
PropRule, // for `from`, `to`, `25%` | ||
} | ||
type GroupByRuleTypes = Map<RuleType, Selector[]> | ||
const rulesByTypes = rulesString.reduce( | ||
(accum, rule): GroupByRuleTypes => { | ||
let ruleType = ((): RuleType|null => { | ||
if (rule.startsWith('@')) return RuleType.AtRule; | ||
if (rule.startsWith(' ')) return RuleType.PropRule; | ||
if (rule.includes('&')) return RuleType.SelectorRule; | ||
return null; | ||
})(); | ||
switch (ruleType) { | ||
case RuleType.PropRule: | ||
rule = rule.slice(1); | ||
break; | ||
case null: | ||
ruleType = RuleType.SelectorRule; | ||
rule = `&${rule}`; | ||
break; | ||
} // switch | ||
let group = accum.get(ruleType); // get an existing collector | ||
if (!group) accum.set(ruleType, group = []); // create a new collector | ||
group.push(rule); | ||
return accum; | ||
}, | ||
new Map<RuleType, Selector[]>() | ||
); | ||
const withCombinator = `&${combinator}`; | ||
const combiSelectors = flat(selectors).map((selector) => { | ||
if (!selector) selector = '*'; // empty selector => match any element | ||
// if (selector === '&') return selector; // no children => the parent itself | ||
if (selector.includes('&')) return selector; // custom combinator | ||
if (((combinator === ' ') || (combinator === '>')) && selector.startsWith('::')) return `&${selector}`; // pseudo element => attach the parent itself (for descendants & children) | ||
return `${withCombinator}${selector}`; | ||
}); | ||
if (!combiSelectors.length) return {}; // no selector => return empty | ||
const selectorList = ( | ||
(rulesByTypes.get(RuleType.SelectorRule) ?? []) | ||
.flatMap((selector) => { | ||
const selectorList = parseSelectors(selector); | ||
if (!selectorList) throw Error(`parse selector error: ${selector}`); | ||
return selectorList; | ||
}) | ||
.filter(isNotEmptySelector) | ||
); | ||
const mergedSelectorList = mergeSelectors(selectorList, options); | ||
const mergedStyles = mergeStyles(styles); // merge the `styles` to single `Style`, for making JSS understand | ||
if (!mergedStyles) return {}; // no style => return empty | ||
if (groupSelectors) { | ||
if (combiSelectors.length === 1) return { | ||
[combiSelectors[0]]: (mergedStyles as JssValue), | ||
}; | ||
return { | ||
...(isNotEmptySelectors(mergedSelectorList) ? { | ||
[Symbol( | ||
selectorsToString(mergedSelectorList) | ||
)] : styles | ||
} : {}), | ||
const selectorsGroups = combiSelectors.map((selector) => { | ||
const withCombi = selector.startsWith(withCombinator); | ||
if (withCombi) return { withCombi: selector }; | ||
const onlyBeginAmp = (selector.lastIndexOf('&') === 0); | ||
if (onlyBeginAmp) return { begAmp: selector }; | ||
const onlyEndAmp = (selector.indexOf('&') === (selector.length - 1)); | ||
if (onlyEndAmp) return { endAmp: selector }; | ||
return { other: selector }; | ||
}); | ||
const withCombiSelectors = selectorsGroups.filter((group) => !!group.withCombi).map((group) => group.withCombi! ); | ||
const begAmpSelectors = selectorsGroups.filter((group) => !!group.begAmp ).map((group) => group.begAmp! ); | ||
const endAmpSelectors = selectorsGroups.filter((group) => !!group.endAmp ).map((group) => group.endAmp! ); | ||
const ungroupableSelectors = selectorsGroups.filter((group) => !!group.other ).map((group) => group.other! ); | ||
return { | ||
...(withCombiSelectors.length ? { | ||
[ | ||
(withCombiSelectors.length === 1) | ||
? | ||
withCombiSelectors[0] | ||
: | ||
`${withCombinator}:is(${withCombiSelectors.map((selector) => selector.slice(withCombinator.length)).join(',')})` | ||
]: (mergedStyles as JssValue), | ||
} : {}), | ||
...(begAmpSelectors.length ? { | ||
[ | ||
(begAmpSelectors.length === 1) | ||
? | ||
begAmpSelectors[0] | ||
: | ||
`&:is(${begAmpSelectors.map((selector) => selector.slice(1)).join(',')})` | ||
]: (mergedStyles as JssValue), | ||
} : {}), | ||
...(endAmpSelectors.length ? { | ||
[ | ||
(endAmpSelectors.length === 1) | ||
? | ||
endAmpSelectors[0] | ||
: | ||
`:is(${endAmpSelectors.map((selector) => selector.slice(0, -1)).join(',')})&` | ||
]: (mergedStyles as JssValue), | ||
} : {}), | ||
...(ungroupableSelectors.length ? { | ||
[ungroupableSelectors.join(',')]: (mergedStyles as JssValue), | ||
} : {}), | ||
}; | ||
} | ||
else { | ||
return Object.fromEntries( | ||
combiSelectors | ||
.map((combiSelector) => [combiSelector, (mergedStyles as JssValue)]) | ||
); | ||
} // if | ||
...Object.fromEntries( | ||
[ | ||
...(rulesByTypes.get(RuleType.AtRule ) ?? []), | ||
...(rulesByTypes.get(RuleType.PropRule ) ?? []), | ||
].map((rule) => [ | ||
Symbol( | ||
rule | ||
), | ||
styles | ||
]), | ||
), | ||
}; | ||
}; | ||
export const descendants = (selectors: SelectorCollection, styles: StyleCollection, options: CombinatorOptions = defaultCombinatorOptions) => combinators(' ', selectors, styles, options); | ||
export const children = (selectors: SelectorCollection, styles: StyleCollection, options: CombinatorOptions = defaultCombinatorOptions) => combinators('>', selectors, styles, options); | ||
export const siblings = (selectors: SelectorCollection, styles: StyleCollection, options: CombinatorOptions = defaultCombinatorOptions) => combinators('~', selectors, styles, options); | ||
export const nextSiblings = (selectors: SelectorCollection, styles: StyleCollection, options: CombinatorOptions = defaultCombinatorOptions) => combinators('+', selectors, styles, options); | ||
// rules: | ||
export interface RuleOptions { | ||
minSpecificityWeight? : number | ||
} | ||
const defaultRuleOptions : Required<RuleOptions> = { | ||
minSpecificityWeight : 0, | ||
}; | ||
export const rules = (ruleCollection: RuleCollection, options: RuleOptions = defaultRuleOptions): StyleCollection => { | ||
const { | ||
minSpecificityWeight = defaultRuleOptions.minSpecificityWeight, | ||
} = options; | ||
// rule groups: | ||
export const rules = (rules : RuleCollection, options: SelectorOptions = defaultSelectorOptions): Rule => { | ||
const result = ( | ||
flat(rules) | ||
.filter((rule): rule is ProductOrFactory<OptionalOrFalse<Rule>> => !!rule) | ||
.flatMap((ruleProductOrFactory: ProductOrFactory<OptionalOrFalse<Rule>>): OptionalOrFalse<Rule>[] => { | ||
if (typeof(ruleProductOrFactory) === 'function') return [ruleProductOrFactory()]; | ||
return [ruleProductOrFactory]; | ||
}) | ||
.filter((optionalRule): optionalRule is Rule => !!optionalRule) | ||
); | ||
if (!options) return Object.assign({}, ...result); | ||
return composition( | ||
((): StyleCollection[] => { | ||
const noSelectors: StyleCollection[] = []; | ||
return [ | ||
...(Array.isArray(ruleCollection) ? ruleCollection : [ruleCollection]) | ||
.flatMap((ruleEntrySourceList: RuleEntrySource|RuleList): OptionalOrFalse<RuleEntry>[] => { // convert: Factory<RuleEntry>|RuleEntry|RuleList => [RuleEntry]|[RuleEntry]|[...RuleList] => [RuleEntry] | ||
const isOptionalString = (value: any): value is OptionalString => { | ||
if (value === null) return true; // optional `null` | ||
if (value === undefined) return true; // optional `undefined` | ||
if (value === false) return true; // optional `false` | ||
return ((typeof value) === 'string'); | ||
}; | ||
const isOptionalStringDeepArr = (value: any): value is OptionalString[] => { | ||
if (!Array.isArray(value)) return false; | ||
const nonOptionalStringItems = value.filter((v) => !isOptionalString(v)); | ||
if (nonOptionalStringItems.length === 0) return true; | ||
for (const nonOptionalStringItem of nonOptionalStringItems) { | ||
if (!isOptionalStringDeepArr(nonOptionalStringItem)) return false; | ||
} // for | ||
return true; | ||
}; | ||
const isOptionalSelector = (value: any): value is OptionalOrFalse<Selector> => isOptionalString(value); | ||
const isOptionalSelectorDeepArr = (value: any): value is OptionalOrFalse<Selector>[] => isOptionalStringDeepArr(value); | ||
const isOptionalStyleOrFactory = (value: any): value is ProductOrFactory<Style> => { | ||
if (value === null) return true; // optional `null` | ||
if (value === undefined) return true; // optional `undefined` | ||
return ( | ||
value | ||
&& | ||
( | ||
((typeof(value) === 'object') && !Array.isArray(value)) // literal object => `Style` | ||
|| | ||
(typeof(value) === 'function') // function => `Factory<Style>` | ||
) | ||
); | ||
}; | ||
const isOptionalStyleOrFactoryDeepArr = (value: any): value is Style[] => { | ||
if (!Array.isArray(value)) return false; | ||
const nonStyleOrFactoryItems = value.filter((v) => !isOptionalStyleOrFactory(v)); | ||
if (nonStyleOrFactoryItems.length === 0) return true; | ||
for (const nonStyleOrFactoryItem of nonStyleOrFactoryItems) { | ||
if (!isOptionalStyleOrFactoryDeepArr(nonStyleOrFactoryItem)) return false; | ||
} // for | ||
return true; | ||
}; | ||
const isOptionalRuleEntry = (value: any): value is OptionalOrFalse<RuleEntry> => { | ||
if (value === null) return true; // optional `null` | ||
if (value === undefined) return true; // optional `undefined` | ||
if (value === false) return true; // optional `false` | ||
if (value.length !== 2) return false; // not a tuple => not a `RuleEntry` | ||
const [first, second] = value; | ||
/* | ||
the first element must be `SelectorCollection`: | ||
* `OptionalOrFalse<Selector>` | ||
* DeepArrayOf< `OptionalOrFalse<Selector>` > | ||
* empty array | ||
*/ | ||
// and | ||
/* | ||
the second element must be `StyleCollection`: | ||
* `OptionalOrFalse<Style>` | `Factory<OptionalOrFalse<Style>>` | ||
* DeepArrayOf< `OptionalOrFalse<Style> | Factory<OptionalOrFalse<Style>>` > | ||
* empty array | ||
*/ | ||
return ( | ||
( | ||
isOptionalSelector(first) | ||
|| | ||
isOptionalSelectorDeepArr(first) | ||
) | ||
&& | ||
( | ||
isOptionalStyleOrFactory(second) | ||
|| | ||
isOptionalStyleOrFactoryDeepArr(second) | ||
) | ||
); | ||
}; | ||
if (typeof(ruleEntrySourceList) === 'function') return [ruleEntrySourceList()]; | ||
if (isOptionalRuleEntry(ruleEntrySourceList)) return [ruleEntrySourceList]; | ||
return ruleEntrySourceList.map((ruleEntrySource) => (typeof(ruleEntrySource) === 'function') ? ruleEntrySource() : ruleEntrySource); | ||
}) | ||
.filter((optionalRuleEntry): optionalRuleEntry is RuleEntry => !!optionalRuleEntry) | ||
.map(([selectors, styles]): readonly [NestedSelector[], StyleCollection] => { | ||
let nestedSelectors = flat(selectors).filter((selector): selector is Selector => !!selector).map((selector): NestedSelector => { | ||
if (selector.startsWith('@')) return (selector as NestedSelector); // for `@media` | ||
if (selector.includes('&')) return (selector as NestedSelector); // &.foo .boo& .foo&.boo | ||
// if (selector.startsWith('.') || selector.startsWith(':') || selector.startsWith('#') || (selector === '*')) return `&${selector}`; | ||
return `&${selector}`; | ||
}); | ||
if (minSpecificityWeight >= 2) { | ||
nestedSelectors = nestedSelectors.map((nestedSelector: NestedSelector): NestedSelector => { | ||
if (nestedSelector === '&') return nestedSelector; // zero specificity => no change | ||
// one/more specificities found => increase the specificity weight until reaches `minSpecificityWeight` | ||
// calculate the specificity weight: | ||
// `.realClassName` or `:pseudoClassName` (without parameters): | ||
const classes = nestedSelector.match(/(\.|:(?!(is|not)(?![\w-])))[\w-]+/gmi); // count the `.RealClass` and `:PseudoClass` but not `:is` or `:not` | ||
const specificityWeight = classes?.length ?? 0; | ||
const missSpecificityWeight = minSpecificityWeight - specificityWeight; | ||
// the specificity weight was meet the minimum specificity required => no change: | ||
if (missSpecificityWeight <= 0) return nestedSelector; | ||
// the specificity weight is less than the minimum specificity required => increase the specificity: | ||
return `${nestedSelector}${(new Array(missSpecificityWeight)).fill(((): Selector => { | ||
const lastClass = classes?.[classes.length - 1]; | ||
if (lastClass) { | ||
// the last word (without parameters): | ||
if (nestedSelector.length === (nestedSelector.lastIndexOf(lastClass) + lastClass.length)) return (lastClass as Selector); // `.RealClass` or `:PseudoClass` without parameters | ||
} // if | ||
// use a **hacky class name** to increase the specificity: | ||
return ':not(._)'; | ||
})()).join('')}` as NestedSelector; | ||
}); | ||
} // if | ||
if (nestedSelectors.includes('&')) { // contains one/more selectors with zero specificity | ||
nestedSelectors = nestedSelectors.filter((nestedSelector) => (nestedSelector !== '&')); // filter out selectors with zero specificity | ||
noSelectors.push(styles); // add styles with zero specificity | ||
} // if | ||
return [nestedSelectors, styles]; | ||
}) | ||
.filter(([nestedSelectors]) => (nestedSelectors.length > 0)) // filter out empty `nestedSelectors` | ||
.map(([nestedSelectors, styles]) => [ | ||
nestedSelectors, | ||
mergeStyles(styles) // merge the `styles` to single `Style`, for making JSS understand | ||
] as const) | ||
.filter((tuple): tuple is (readonly [typeof tuple[0], Style]) => !!tuple[1]) // filter out empty `mergedStyles` | ||
.map(([nestedSelectors, mergedStyles]): Style => { | ||
const selectorsGroups = nestedSelectors.map((selector) => { | ||
const onlyBeginAmp = (selector.lastIndexOf('&') === 0); | ||
if (onlyBeginAmp) return { begAmp: selector }; | ||
const onlyEndAmp = (selector.indexOf('&') === (selector.length - 1)); | ||
if (onlyEndAmp) return { endAmp: selector }; | ||
return { other: selector }; | ||
}); | ||
const begAmpSelectors = selectorsGroups.filter((group) => !!group.begAmp ).map((group) => group.begAmp! ); | ||
const endAmpSelectors = selectorsGroups.filter((group) => !!group.endAmp ).map((group) => group.endAmp! ); | ||
const ungroupableSelectors = selectorsGroups.filter((group) => !!group.other ).map((group) => group.other! ); | ||
return { | ||
...(begAmpSelectors.length ? { | ||
[ | ||
(begAmpSelectors.length === 1) | ||
? | ||
begAmpSelectors[0] | ||
: | ||
`&:is(${begAmpSelectors.map((selector) => selector.slice(1)).join(',')})` | ||
]: (mergedStyles as JssValue), | ||
} : {}), | ||
...(endAmpSelectors.length ? { | ||
[ | ||
(endAmpSelectors.length === 1) | ||
? | ||
endAmpSelectors[0] | ||
: | ||
`:is(${endAmpSelectors.map((selector) => selector.slice(0, -1)).join(',')})&` | ||
]: (mergedStyles as JssValue), | ||
} : {}), | ||
...(ungroupableSelectors.length ? { | ||
[ungroupableSelectors.join(',')]: (mergedStyles as JssValue), | ||
} : {}), | ||
}; | ||
}), | ||
...noSelectors, | ||
]; | ||
})() | ||
return Object.assign({}, | ||
...result | ||
.flatMap((rule) => Object.getOwnPropertySymbols(rule).map((sym) => [sym.description ?? '', rule[sym]] as const)) | ||
.map(([selectors, styles]) => rule(selectors, styles, options)) | ||
); | ||
}; | ||
// shortcut rules: | ||
/** | ||
* Defines component's variants. | ||
* @returns A `StyleCollection` represents the component's variants. | ||
* @returns A `Rule` represents the component's variants. | ||
*/ | ||
export const variants = (variants: RuleCollection, options: RuleOptions = defaultRuleOptions): StyleCollection => rules(variants, options); | ||
export const variants = (variants: RuleCollection, options: SelectorOptions = defaultSelectorOptions) => rules(variants, options); | ||
export interface StateOptions extends SelectorOptions { | ||
inherit ?: boolean | ||
} | ||
const defaultStateOptions : Required<StateOptions> = { | ||
...defaultSelectorOptions, | ||
minSpecificityWeight: 3, | ||
inherit : false, | ||
}; | ||
/** | ||
* Defines component's states. | ||
* @param inherit `true` to inherit states from parent element -or- `false` to create independent states. | ||
* @returns A `StyleCollection` represents the component's states. | ||
* @returns A `Rule` represents the component's states. | ||
*/ | ||
export const states = (states: RuleCollection|((inherit: boolean) => RuleCollection), inherit = false, options: RuleOptions = { ...defaultRuleOptions, minSpecificityWeight: 3 }): StyleCollection => { | ||
export const states = (states : RuleCollection|((inherit: boolean) => RuleCollection), options: StateOptions = defaultStateOptions) => { | ||
const { | ||
inherit = defaultStateOptions.inherit, | ||
} = options; | ||
return rules((typeof(states) === 'function') ? states(inherit) : states, options); | ||
} | ||
// rule items: | ||
/** | ||
* Defines component's `style(s)` that is applied when the specified `selector(s)` meet the conditions. | ||
* @returns A `RuleEntry` represents the component's rule. | ||
*/ | ||
export const rule = (selectors: SelectorCollection, styles: StyleCollection): RuleEntry => [selectors, styles]; | ||
// shortcut rule items: | ||
export const noRule = (styles: StyleCollection) => rule('&' , styles); | ||
export const emptyRule = () => rule(null , null ); | ||
export const atRoot = (styles: StyleCollection) => rule(':root' , styles); | ||
export const atGlobal = (styles: StyleCollection) => rule('@global' , styles); | ||
export const fontFace = (styles: StyleCollection) => atGlobal( | ||
rules([ | ||
rule('@font-face', styles), | ||
]), | ||
); | ||
export const isFirstChild = (styles: StyleCollection) => rule( ':first-child' , styles); | ||
export const isNotFirstChild = (styles: StyleCollection) => rule(':not(:first-child)' , styles); | ||
export const isLastChild = (styles: StyleCollection) => rule( ':last-child' , styles); | ||
export const isNotLastChild = (styles: StyleCollection) => rule(':not(:last-child)' , styles); | ||
export const isNthChild = (step: number, offset: number, styles: StyleCollection): RuleEntry => { | ||
// rule shortcuts: | ||
export const keyframes = (name: string, items: PropEx.Keyframes) => rule(`@keyframes ${name}`, (Object.fromEntries( | ||
Object.entries(items).map(([key, frame]) => [Symbol(key), frame]) | ||
) as Style)); | ||
export const noRule = (...styles: StyleCollection[]) => rule('&' , styles); | ||
export const emptyRule = () => rule(null , null ); | ||
export const fallbacks = (...styles: StyleCollection[]) => rule('@fallbacks' , styles); | ||
export const fontFace = (...styles: StyleCollection[]) => rule('@font-face' , styles); | ||
export const atGlobal = (...rules : RuleCollection[]) => rule('@global' , rules ); | ||
export const atRoot = (...styles: StyleCollection[]) => rule(':root' , styles); | ||
export const isFirstChild = (...styles: StyleCollection[]) => rule( ':first-child' , styles); | ||
export const isNotFirstChild = (...styles: StyleCollection[]) => rule(':not(:first-child)' , styles); | ||
export const isLastChild = (...styles: StyleCollection[]) => rule( ':last-child' , styles); | ||
export const isNotLastChild = (...styles: StyleCollection[]) => rule(':not(:last-child)' , styles); | ||
export const isNthChild = (step: number, offset: number, ...styles: StyleCollection[]): Rule => { | ||
if (step === 0) { // no step | ||
if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) return emptyRule(); // 0th => never => return empty rule | ||
if (offset === 1) return isFirstChild(styles); | ||
if (offset === 1) return isFirstChild(styles); // 1st | ||
return rule(`:nth-child(${offset})`, styles); | ||
return rule(`:nth-child(${offset})`, styles); // 2nd, 3rd, 4th, ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) return rule(`:nth-child(n)`, styles); // always match | ||
return rule(`:nth-child(n+${offset})`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) return rule(`:nth-child(${step}n)`, styles); | ||
return rule(`:nth-child(${step}n+${offset})`, styles); | ||
} // if | ||
}; | ||
export const isNotNthChild = (step: number, offset: number, styles: StyleCollection): RuleEntry => { | ||
export const isNotNthChild = (step: number, offset: number, ...styles: StyleCollection[]): Rule => { | ||
if (step === 0) { // no step | ||
// if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) return isNthChild(1, 0, styles); // not 0th => not never => always match | ||
if (offset === 1) return isNotFirstChild(styles); | ||
if (offset === 1) return isNotFirstChild(styles); // not 1st | ||
return rule(`:not(:nth-child(${offset}))`, styles); | ||
return rule(`:not(:nth-child(${offset}))`, styles); // not 2nd, not 3rd, not 4th, not ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) return emptyRule(); // never match | ||
return rule(`:not(:nth-child(n+${offset}))`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) return rule(`:not(:nth-child(${step}n))`, styles); | ||
return rule(`:not(:nth-child(${step}n+${offset}))`, styles); | ||
} // if | ||
}; | ||
export const isNthLastChild = (step: number, offset: number, styles: StyleCollection): RuleEntry => { | ||
export const isNthLastChild = (step: number, offset: number, ...styles: StyleCollection[]): Rule => { | ||
if (step === 0) { // no step | ||
if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) return emptyRule(); // 0th => never => return empty rule | ||
if (offset === 1) return isLastChild(styles); | ||
if (offset === 1) return isLastChild(styles); // 1st | ||
return rule(`:nth-last-child(${offset})`, styles); | ||
return rule(`:nth-last-child(${offset})`, styles); // 2nd, 3rd, 4th, ... | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) return rule(`:nth-last-child(n)`, styles); // always match | ||
return rule(`:nth-last-child(n+${offset})`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) return rule(`:nth-last-child(${step}n)`, styles); | ||
return rule(`:nth-last-child(${step}n+${offset})`, styles); | ||
} // if | ||
}; | ||
export const isNotNthLastChild = (step: number, offset: number, styles: StyleCollection): RuleEntry => { | ||
export const isNotNthLastChild = (step: number, offset: number, ...styles: StyleCollection[]): Rule => { | ||
if (step === 0) { // no step | ||
// if (offset === 0) return emptyRule(); // element indices are starting from 1 => never match => return empty style | ||
if (offset === 0) return isNthChild(1, 0, styles); // not 0th last => not never => always match | ||
if (offset === 1) return isNotLastChild(styles); | ||
if (offset === 1) return isNotLastChild(styles); // not 1st last | ||
return rule(`:not(:nth-last-child(${offset}))`, styles); | ||
return rule(`:not(:nth-last-child(${offset}))`, styles); // not 2nd last, not 3rd last, not 4th last, not ... last | ||
} | ||
else if (step === 1) { // 1 step | ||
if (offset === 0) return emptyRule(); // never match | ||
return rule(`:not(:nth-last-child(n+${offset}))`, styles); | ||
} | ||
else { // 2+ steps | ||
if (offset === 0) return rule(`:not(:nth-last-child(${step}n))`, styles); | ||
return rule(`:not(:nth-last-child(${step}n+${offset}))`, styles); | ||
} // if | ||
}; | ||
export const isActive = (styles: StyleCollection) => rule( ':active' , styles); | ||
export const isNotActive = (styles: StyleCollection) => rule(':not(:active)' , styles); | ||
export const isFocus = (styles: StyleCollection) => rule( ':focus' , styles); | ||
export const isNotFocus = (styles: StyleCollection) => rule(':not(:focus)' , styles); | ||
export const isFocusVisible = (styles: StyleCollection) => rule( ':focus-visible' , styles); | ||
export const isNotFocusVisible = (styles: StyleCollection) => rule(':not(:focus-visible)', styles); | ||
export const isHover = (styles: StyleCollection) => rule( ':hover' , styles); | ||
export const isNotHover = (styles: StyleCollection) => rule(':not(:hover)' , styles); | ||
export const isEmpty = (styles: StyleCollection) => rule( ':empty' , styles); | ||
export const isNotEmpty = (styles: StyleCollection) => rule(':not(:empty)' , styles); | ||
export const isActive = (...styles: StyleCollection[]) => rule( ':active' , styles); | ||
export const isNotActive = (...styles: StyleCollection[]) => rule(':not(:active)' , styles); | ||
export const isFocus = (...styles: StyleCollection[]) => rule( ':focus' , styles); | ||
export const isNotFocus = (...styles: StyleCollection[]) => rule(':not(:focus)' , styles); | ||
export const isFocusVisible = (...styles: StyleCollection[]) => rule( ':focus-visible' , styles); | ||
export const isNotFocusVisible = (...styles: StyleCollection[]) => rule(':not(:focus-visible)', styles); | ||
export const isHover = (...styles: StyleCollection[]) => rule( ':hover' , styles); | ||
export const isNotHover = (...styles: StyleCollection[]) => rule(':not(:hover)' , styles); | ||
export const isEmpty = (...styles: StyleCollection[]) => rule( ':empty' , styles); | ||
export const isNotEmpty = (...styles: StyleCollection[]) => rule(':not(:empty)' , styles); | ||
//combinators: | ||
export const combinators = (combinator: Combinator, selectors: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions): Rule => { | ||
const combiSelectors : Selector[] = flat(selectors).filter((selector): selector is Selector => !!selector).map((selector) => { | ||
// if (selector === '&') return selector; // no children => the parent itself | ||
if (selector.includes('&')) return selector; // custom combinator | ||
if (((combinator === ' ') || (combinator === '>')) && selector.startsWith('::')) return `&${selector}`; // pseudo element => attach the parent itself (for descendants & children) | ||
return `&${combinator}${selector}`; | ||
}); | ||
if (!combiSelectors.length) return {}; // no selector => return empty | ||
return rule(combiSelectors, styles, options); | ||
}; | ||
export const descendants = (selectors: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions) => combinators(' ', selectors, styles, options); | ||
export const children = (selectors: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions) => combinators('>', selectors, styles, options); | ||
export const siblings = (selectors: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions) => combinators('~', selectors, styles, options); | ||
export const nextSiblings = (selectors: SelectorCollection, styles: StyleCollection, options: SelectorOptions = defaultSelectorOptions) => combinators('+', selectors, styles, options); | ||
// utilities: | ||
@@ -803,18 +1255,5 @@ /** | ||
const merged: T[] = []; | ||
for (const item of collection) { | ||
if (Array.isArray(item)) { | ||
// an array => DeepArray<T> => recursively `flat()` | ||
merged.push(...flat(item)); | ||
} | ||
else { | ||
// not an array => T | ||
merged.push(item); | ||
} // if | ||
} // for | ||
return merged; | ||
return collection.flat(Infinity); | ||
}; | ||
export const iif = <T extends PropList|Style>(condition: boolean, content: T): T => { | ||
export const iif = <T extends CssProps|Rule|Style>(condition: boolean, content: T): T => { | ||
return condition ? content : ({} as T); | ||
@@ -821,0 +1260,0 @@ }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
120511
2092
2
+ Added@cssfn/css-selector@^1.0.4
+ Addedcsstype@^3.0.10
- Removed@cssfn/jss-plugin-extend@^1.0.1
- Removed@cssfn/jss-plugin-global@^1.0.3
- Removed@cssfn/jss-plugin-global@1.0.3(transitive)
Updated@cssfn/css-types@^1.0.4
Updated@cssfn/types@^1.0.4