@stylable/core
Version:
CSS for Components
253 lines (237 loc) • 8.02 kB
text/typescript
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<string, ImmutableSelectorList>();
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<T extends ImmutableSelector | ImmutableSelectorList>(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>): 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<ImmutableSelectorNode>) {
for (const parent of parents) {
if (parent.type === `pseudo_class`) {
return true;
}
}
return false;
}
export function matchTypeAndValue(
a: Partial<ImmutableSelectorNode>,
b: Partial<ImmutableSelectorNode>
) {
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,
};
}