import { parseCssSelector, stringifySelectorAst, walk, SelectorNode, PseudoClass, Selector, SelectorList, FunctionalSelector, Class, Attribute, Invalid, ImmutableSelector, ImmutableSelectorList, ImmutableSelectorNode, Combinator, } from '@tokey/css-selector-parser'; import cloneDeep from 'lodash.clonedeep'; export const parseSelector = parseCssSelector; export const stringifySelector = stringifySelectorAst; export const walkSelector = walk; /** * parse selectors and cache them */ const selectorAstCache = new Map(); export function parseSelectorWithCache(selector: string, options: { clone: true }): SelectorList; export function parseSelectorWithCache( selector: string, options?: { clone?: false } ): ImmutableSelectorList; export function parseSelectorWithCache( selector: string, options: { clone?: boolean } = {} ): ImmutableSelectorList { if (!selectorAstCache.has(selector)) { if (selectorAstCache.size > 10000) { selectorAstCache.delete(selectorAstCache.keys().next().value); } selectorAstCache.set(selector, parseCssSelector(selector)); } const cachedValue = selectorAstCache.get(selector); return options.clone ? (cloneDeep(cachedValue) as SelectorList) : (cachedValue as ImmutableSelectorList); } export function cloneSelector(s: T): T { return cloneDeep(s); } /** * returns for each selector if it contains only * a single class or an element selector. */ export function isSimpleSelector(selector: string): { isSimple: boolean; type: 'class' | 'type' | 'complex'; }[] { const selectorList = parseSelectorWithCache(selector); return selectorList.map((selector) => { let foundType = ``; walk( selector, (node) => { if ((node.type !== `class` && node.type !== `type`) || foundType || node.nodes) { foundType = `complex`; return walk.stopAll; } foundType = node.type; return; }, { ignoreList: [`selector`, `comment`] } ); if (foundType === `class` || foundType === `type`) { return { type: foundType, isSimple: true }; } else { return { type: `complex`, isSimple: false }; } }); } /** * take an ast node with nested nodes "XXX(nest1, nest2)" * and convert it to a flat selector as node: "nest1, nest2" */ export function flattenFunctionalSelector(node: FunctionalSelector): Selector { node.value = ``; return convertToSelector(node); } /** * ast convertors */ export function convertToClass(node: SelectorNode): Class { const castedNode = node as Class; castedNode.type = `class`; castedNode.dotComments = []; return castedNode; } export function convertToAttribute(node: SelectorNode): Attribute { const castedNode = node as Attribute; castedNode.type = `attribute`; return castedNode; } export function convertToInvalid(node: SelectorNode): Invalid { const castedNode = node as Invalid; castedNode.type = `invalid`; return castedNode; } export function convertToSelector(node: SelectorNode): Selector { const castedNode = node as Selector; castedNode.type = `selector`; castedNode.before ||= ``; castedNode.after ||= ``; // ToDo: should this fix castedNode.end? return castedNode; } export function convertToPseudoClass( node: SelectorNode, name: string, nestedSelectors?: SelectorList ): PseudoClass { const castedNode = node as PseudoClass; castedNode.type = 'pseudo_class'; castedNode.value = name; castedNode.colonComments = []; if (nestedSelectors) { castedNode.nodes = nestedSelectors; } else { delete castedNode.nodes; } return castedNode; } export function createCombinatorSelector(partial: Partial): Combinator { const type = partial.combinator || 'space'; return { type: `combinator`, combinator: type, value: partial.value ?? (type === 'space' ? ` ` : type), before: partial.before ?? ``, after: partial.after ?? ``, start: partial.start ?? 0, end: partial.end ?? 0, invalid: partial.invalid ?? false, }; } export function isInPseudoClassContext(parents: ReadonlyArray) { for (const parent of parents) { if (parent.type === `pseudo_class`) { return true; } } return false; } export function matchTypeAndValue( a: Partial, b: Partial ) { return a.type === b.type && (a as any).value === (b as any).value; } export function isCompRoot(name: string) { return name.charAt(0).match(/[A-Z]/); } const isNestedNode = (node: SelectorNode) => node.type === 'nesting'; /** * combine 2 selector lists. * - add each scoping selector at the begging of each nested selector * - replace any nesting `&` nodes in the nested selector with the scoping selector nodes */ export function scopeNestedSelector( scopeSelectorAst: ImmutableSelectorList, nestedSelectorAst: ImmutableSelectorList, rootScopeLevel = false, isAnchor: (node: SelectorNode) => boolean = isNestedNode ): { selector: string; ast: SelectorList } { const resultSelectors: SelectorList = []; nestedSelectorAst.forEach((targetAst) => { scopeSelectorAst.forEach((scopeAst) => { const outputAst = cloneDeep(targetAst) as Selector; outputAst.before = scopeAst.before || outputAst.before; let first = outputAst.nodes[0]; // search first actual first selector part walkSelector( outputAst, (node) => { first = node; return walkSelector.stopAll; }, { ignoreList: [`selector`] } ); // merge scope flags const nestStartWithNesting = first.type === `nesting`; const nestedStartWithGlobal = rootScopeLevel && first.type === `pseudo_class` && first.value === `global`; const nestStartWithScope = rootScopeLevel && scopeAst.nodes.every((node, i) => { return matchTypeAndValue(node, outputAst.nodes[i]); }); let scopeAlreadyMerged = false; // merge scope into selector walkSelector(outputAst, (node, i, nodes) => { if (isAnchor(node)) { scopeAlreadyMerged = true; nodes.splice(i, 1, { type: `selector`, nodes: cloneDeep(scopeAst.nodes as SelectorNode[]), start: node.start, end: node.end, after: ``, before: ``, }); } }); // merge scope at the beginning of selector if ( first && !nestStartWithNesting && !nestStartWithScope && !nestedStartWithGlobal && !scopeAlreadyMerged ) { outputAst.nodes.unshift(...cloneDeep(scopeAst.nodes as SelectorNode[]), { type: `combinator`, combinator: `space`, value: ` `, before: ``, after: ``, start: first.start, end: first.start, invalid: false, }); } resultSelectors.push(outputAst); }); }); return { selector: stringifySelector(resultSelectors), ast: resultSelectors, }; }