@stylable/core
Version:
CSS for Components
239 lines (228 loc) • 8.71 kB
text/typescript
import {
parseSelectorWithCache,
stringifySelector,
scopeNestedSelector,
walkSelector,
convertToSelector,
matchTypeAndValue,
isSimpleSelector,
} from './selector';
import {
Selector,
ImmutableSelectorNode,
groupCompoundSelectors,
SelectorList,
SelectorNode,
} from '@tokey/css-selector-parser';
import * as postcss from 'postcss';
import { transformInlineCustomSelectors } from './custom-selector';
export function isChildOfAtRule(rule: postcss.Container, atRuleName: string) {
let currentParent = rule.parent;
while (currentParent) {
if (
currentParent.type === 'atrule' &&
(currentParent as postcss.AtRule).name === atRuleName
) {
return true;
}
currentParent = currentParent.parent;
}
return false;
}
export function isInConditionalGroup(node: postcss.Rule | postcss.AtRule, includeRoot = true) {
// https://www.w3.org/TR/css-conditional-3/#contents-of
const parent = node.parent as any;
return (
parent &&
((includeRoot && parent.type === `root`) ||
(parent.type === `atrule` && (parent.name === `media` || parent.name === `supports`)))
);
}
export function createSubsetAst<T extends postcss.Root | postcss.AtRule | postcss.Rule>(
root: postcss.Root | postcss.AtRule | postcss.Rule,
selectorPrefix: string,
mixinTarget?: T,
isRoot = false,
getCustomSelector?: (name: string) => SelectorList | undefined,
isNestedInMixin = false
): T {
// keyframes on class mixin?
const prefixSelectorList = parseSelectorWithCache(selectorPrefix);
const prefixType = prefixSelectorList[0].nodes[0];
const containsPrefix = containsMatchInFirstChunk.bind(null, prefixType);
const mixinRoot = mixinTarget ? mixinTarget : postcss.root();
root.nodes?.forEach((node) => {
if (node.type === 'decl') {
mixinTarget?.append(node.clone());
} else if (
node.type === `rule` &&
(node.selector === ':vars' || node.selector === ':import')
) {
// nodes that don't mix
return;
} else if (node.type === `rule`) {
const selectorAst = parseSelectorWithCache(node.selector, { clone: true });
let ast = isRoot
? scopeNestedSelector(prefixSelectorList, selectorAst, true).ast
: selectorAst;
if (getCustomSelector) {
ast = transformInlineCustomSelectors(ast, getCustomSelector, () => {
/*don't report*/
});
}
const matchesSelectors =
isRoot || isNestedInMixin ? ast : ast.filter((node) => containsPrefix(node));
if (matchesSelectors.length) {
const selector = stringifySelector(
matchesSelectors.map((selectorNode) => {
if (!isRoot) {
selectorNode = fixChunkOrdering(selectorNode, prefixType);
}
replaceTargetWithMixinAnchor(selectorNode, prefixType);
return selectorNode;
})
);
const clonedRule = createSubsetAst(
node,
selectorPrefix,
node.clone({ selector, nodes: [] }),
isRoot,
getCustomSelector,
true /*isNestedInMixin*/
);
mixinRoot.append(clonedRule);
}
} else if (node.type === `atrule`) {
if (
node.name === 'media' ||
node.name === 'supports' ||
node.name === 'st-scope' ||
node.name === 'layer' ||
node.name === 'container'
) {
let scopeSelector = node.name === 'st-scope' ? node.params : '';
let atruleHasMixin = isNestedInMixin || false;
if (scopeSelector) {
const ast = parseSelectorWithCache(scopeSelector, { clone: true });
const matchesSelectors = isRoot
? ast
: ast.filter((node) => containsPrefix(node));
if (matchesSelectors.length) {
atruleHasMixin = true;
scopeSelector = stringifySelector(
matchesSelectors.map((selectorNode) => {
if (!isRoot) {
selectorNode = fixChunkOrdering(selectorNode, prefixType);
}
replaceTargetWithMixinAnchor(selectorNode, prefixType);
return selectorNode;
})
);
}
}
const atRuleSubset = createSubsetAst(
node,
selectorPrefix,
postcss.atRule({
params: scopeSelector || node.params,
name: node.name,
}),
isRoot,
getCustomSelector,
atruleHasMixin
);
if (atRuleSubset.nodes) {
mixinRoot.append(atRuleSubset);
}
} else if (isRoot) {
mixinRoot.append(node.clone());
}
} else {
// TODO: add warn?
}
});
return mixinRoot as T;
}
export const stMixinMarker = 'st-mixin-marker';
export const isStMixinMarker = (node: SelectorNode) =>
node.type === 'attribute' && node.value === stMixinMarker;
function replaceTargetWithMixinAnchor(selectorNode: Selector, prefixType: ImmutableSelectorNode) {
walkSelector(selectorNode, (node) => {
if (matchTypeAndValue(node, prefixType)) {
convertToSelector(node).nodes = [
{
type: `attribute`,
value: stMixinMarker,
start: node.start,
end: node.end,
},
];
}
});
}
function fixChunkOrdering(selectorNode: Selector, prefixType: ImmutableSelectorNode) {
const compound = groupCompoundSelectors(selectorNode, {
deep: true,
splitPseudoElements: false,
});
walkSelector(compound, (node) => {
if (node.type === `compound_selector`) {
const simpleNodes = node.nodes;
for (let i = 1; i < simpleNodes.length; i++) {
const childNode = simpleNodes[i];
if (matchTypeAndValue(childNode, prefixType)) {
const chunk = simpleNodes.splice(i, simpleNodes.length - i);
simpleNodes.unshift(...chunk);
break;
}
}
}
});
return compound;
}
function containsMatchInFirstChunk(
prefixType: ImmutableSelectorNode,
selectorNode: ImmutableSelectorNode
) {
let isMatch = false;
walkSelector(selectorNode, (node) => {
if (node.type === `combinator`) {
return walkSelector.stopAll;
} else if (node.type === 'pseudo_class') {
// handle nested match :is(.mix)
if (node.nodes) {
for (const innerSelectorNode of node.nodes) {
if (containsMatchInFirstChunk(prefixType, innerSelectorNode)) {
isMatch = true;
}
}
}
return walkSelector.skipNested;
} else if (matchTypeAndValue(node, prefixType)) {
isMatch = true;
return walkSelector.stopAll;
}
return;
});
return isMatch;
}
/** @deprecated internal for transformer */
export function findRule(
root: postcss.Root,
selector: string,
test: any = (statement: any) => statement.prop === `-st-extends`
): null | postcss.Declaration {
let found: any = null;
root.walkRules(selector, (rule) => {
const declarationIndex = rule.nodes ? rule.nodes.findIndex(test) : -1;
const isSimplePerSelector = isSimpleSelector(rule.selector);
// This will assume that a selector that contains .a, .b:hover is simple! (for backward compatibility)
const isSimple = isSimplePerSelector.reduce((acc, { isSimple }) => {
return !isSimple ? false : acc;
}, true);
if (isSimple && !!~declarationIndex) {
found = rule.nodes[declarationIndex];
}
});
return found;
}