@stylable/core
Version:
CSS for Components
335 lines (317 loc) • 11.4 kB
text/typescript
import type * as postcss from 'postcss';
import { Diagnostics } from './diagnostics';
import { knownPseudoClassesWithNestedSelectors } from './native-reserved-lists';
import { StylableMeta } from './stylable-meta';
import { CSSCustomProperty, STVar, STCustomSelector } from './features';
import { generalDiagnostics } from './features/diagnostics';
import {
FeatureContext,
STSymbol,
STImport,
STNamespace,
STGlobal,
STScope,
CSSClass,
CSSType,
CSSKeyframes,
CSSLayer,
CSSContains,
STStructure,
} from './features';
import { processDeclarationFunctions } from './process-declaration-functions';
import {
walkSelector,
isInPseudoClassContext,
parseSelectorWithCache,
stringifySelector,
} from './helpers/selector';
import { isChildOfAtRule } from './helpers/rule';
import { defaultFeatureFlags, type FeatureFlags } from './features/feature';
export class StylableProcessor implements FeatureContext {
public meta!: StylableMeta;
constructor(
public diagnostics = new Diagnostics(),
private resolveNamespace = STNamespace.defaultProcessNamespace,
public flags: FeatureFlags = { ...defaultFeatureFlags }
) {}
public process(root: postcss.Root): StylableMeta {
this.meta = new StylableMeta(root, this.diagnostics, this.flags);
STStructure.hooks.analyzeInit(this);
STImport.hooks.analyzeInit(this);
CSSCustomProperty.hooks.analyzeInit(this);
this.handleAtRules(root);
root.walkRules((rule) => {
if (!isChildOfAtRule(rule, 'keyframes')) {
this.handleRule(rule, {
isScoped: isChildOfAtRule(rule, `st-scope`),
reportUnscoped: true,
});
}
});
root.walkDecls((decl) => {
CSSClass.hooks.analyzeDeclaration({ context: this, decl });
CSSCustomProperty.hooks.analyzeDeclaration({ context: this, decl });
CSSContains.hooks.analyzeDeclaration({ context: this, decl });
this.collectUrls(decl);
});
STNamespace.hooks.analyzeDone(this);
STCustomSelector.hooks.analyzeDone(this);
STStructure.hooks.analyzeDone(this);
STNamespace.setMetaNamespace(this, this.resolveNamespace);
STSymbol.reportRedeclare(this);
return this.meta;
}
protected handleAtRules(root: postcss.Root) {
const analyzeRule = (
rule: postcss.Rule,
{
isScoped,
originalNode,
}: { isScoped: boolean; originalNode: postcss.AtRule | postcss.Rule }
) => {
return this.handleRule(rule, {
isScoped,
originalNode,
reportUnscoped: false,
});
};
root.walkAtRules((atRule) => {
switch (atRule.name) {
case 'st-import': {
STImport.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
case 'namespace':
case 'st-namespace': {
STNamespace.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
case 'keyframes':
CSSKeyframes.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
case 'layer':
CSSLayer.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
case 'import':
STImport.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
CSSLayer.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
case 'custom-selector': {
STCustomSelector.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
case 'st-scope':
STScope.hooks.analyzeAtRule({ context: this, atRule, analyzeRule });
break;
case 'property':
case 'st-global-custom-property': {
CSSCustomProperty.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
case 'container': {
CSSContains.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
case 'st': {
STStructure.hooks.analyzeAtRule({
context: this,
atRule,
analyzeRule,
});
break;
}
}
});
}
private collectUrls(decl: postcss.Declaration) {
processDeclarationFunctions(
decl,
(node) => {
if (node.type === 'url') {
this.meta.urls.push(node.url);
}
},
false
);
}
protected handleRule(
rule: postcss.Rule,
{
isScoped,
reportUnscoped,
originalNode = rule,
}: {
isScoped: boolean;
reportUnscoped: boolean;
originalNode?: postcss.AtRule | postcss.Rule;
}
) {
const selectorAst = parseSelectorWithCache(rule.selector);
let locallyScoped = isScoped;
let topSelectorIndex = -1;
walkSelector(selectorAst, (node, ...nodeContext) => {
const [index, nodes, parents] = nodeContext;
const type = node.type;
if (type === 'selector' && !isInPseudoClassContext(parents)) {
// reset scope check between top level selectors
locallyScoped = isScoped;
topSelectorIndex++;
}
const walkSkip = STGlobal.hooks.analyzeSelectorNode({
context: this,
node,
topSelectorIndex,
rule,
originalNode,
walkContext: nodeContext,
});
if (walkSkip !== undefined) {
return walkSkip;
}
if (node.type === 'pseudo_class') {
if (node.value === 'import') {
STImport.hooks.analyzeSelectorNode({
context: this,
node,
topSelectorIndex,
rule,
originalNode,
walkContext: nodeContext,
});
} else if (node.value === 'vars') {
return STVar.hooks.analyzeSelectorNode({
context: this,
node,
topSelectorIndex,
rule,
originalNode,
walkContext: nodeContext,
});
} else if (node.value.startsWith('--')) {
// ToDo: move to css-class feature
locallyScoped =
locallyScoped ||
STCustomSelector.isScoped(this.meta, node.value.slice(2)) ||
false;
} else if (!knownPseudoClassesWithNestedSelectors.includes(node.value)) {
return walkSelector.skipNested;
}
} else if (node.type === 'class') {
CSSClass.hooks.analyzeSelectorNode({
context: this,
node,
topSelectorIndex,
rule,
originalNode,
walkContext: nodeContext,
});
locallyScoped = CSSClass.validateClassScoping({
context: this,
classSymbol: CSSClass.get(this.meta, node.value)!,
locallyScoped,
reportUnscoped,
node,
nodes,
index,
rule,
});
} else if (node.type === 'type') {
CSSType.hooks.analyzeSelectorNode({
context: this,
node,
topSelectorIndex,
rule,
originalNode,
walkContext: nodeContext,
});
locallyScoped = CSSType.validateTypeScoping({
context: this,
locallyScoped,
reportUnscoped,
node,
nodes,
index,
rule,
});
} else if (node.type === `id`) {
if (node.nodes) {
this.diagnostics.report(
generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR(`#` + node.value, `id`),
{
node: rule,
word: stringifySelector(node),
}
);
}
} else if (node.type === `attribute`) {
if (node.nodes) {
this.diagnostics.report(
generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR(
`[${node.value}]`,
`attribute`
),
{
node: rule,
word: stringifySelector(node),
}
);
}
} else if (node.type === `nesting`) {
if (node.nodes) {
this.diagnostics.report(
generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR(node.value, `nesting`),
{
node: rule,
word: stringifySelector(node),
}
);
}
}
return;
});
STGlobal.hooks.analyzeSelectorDone({
context: this,
rule,
originalNode,
});
return locallyScoped;
}
}
// ToDo: remove export and reroute import from feature
export const processNamespace = STNamespace.defaultProcessNamespace;