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;