import { createFeature } from './feature'; import { parseSelectorWithCache, scopeNestedSelector } from '../helpers/selector'; import type { Stylable } from '../stylable'; import type { ImmutablePseudoClass } from '@tokey/css-selector-parser'; import * as postcss from 'postcss'; import type { SRule } from '../deprecated/postcss-ast-extension'; export const diagnostics = { // INVALID_SCOPING: createDiagnosticReporter( // '11009', // 'error', // () => '"@st-scope" requires a valid selector or empty value' // ), }; // HOOKS export const hooks = createFeature<{ IMMUTABLE_SELECTOR: ImmutablePseudoClass }>({ analyzeAtRule({ context, atRule, analyzeRule }) { if (!isStScopeStatement(atRule)) { return; } // notice: any value from params would be taken as a scoping // selector to be prepended to nested selectors analyzeRule( postcss.rule({ selector: atRule.params, source: atRule.source, }), { isScoped: true, originalNode: atRule, } ); context.meta.scopes.push(atRule); }, prepareAST({ node, toRemove }) { // called with experimentalSelectorInference=false // flatten @st-scope before transformation if (isStScopeStatement(node)) { flattenScope(node); toRemove.push(() => node.replaceWith(node.nodes || [])); } }, transformAtRuleNode({ context: { meta, inferredSelectorMixin }, atRule, transformer }) { if (isStScopeStatement(atRule)) { const { selector, inferredSelector } = transformer.scopeSelector( meta, atRule.params, atRule, undefined, inferredSelectorMixin ); // transform selector in params atRule.params = selector; // track selector context for nested selector nodes transformer.containerInferredSelectorMap.set(atRule, inferredSelector); } }, transformLastPass({ ast }) { // called with experimentalSelectorInference=true // flatten @st-scope after transformation const toRemove = []; for (const node of ast.nodes) { if (isStScopeStatement(node)) { flattenScope(node); toRemove.push(() => node.replaceWith(node.nodes || [])); } } toRemove.forEach((remove) => remove()); }, }); // API export class StylablePublicApi { constructor(private stylable: Stylable) {} public getStScope(rule: postcss.Rule) { return getStScope(rule); } } export function isStScopeStatement(node: any): node is postcss.AtRule { return node.type === 'atrule' && node.name === 'st-scope'; } function flattenScope(atRule: postcss.AtRule) { const scopeSelector = atRule.params; if (scopeSelector) { atRule.walkRules((rule) => { rule.selector = scopeNestedSelector( parseSelectorWithCache(scopeSelector), parseSelectorWithCache(rule.selector) ).selector; (rule as SRule).stScopeSelector = atRule.params; }); } } function getStScope(rule: postcss.Rule): postcss.AtRule | undefined { let current: postcss.Container | postcss.Document = rule; while (current?.parent) { current = current.parent; if (isStScopeStatement(current) && current.parent?.type === 'root') { return current; } } return; }