@stylable/core
Version:
CSS for Components
855 lines (813 loc) • 28.9 kB
text/typescript
import { plugableRecord } from '../helpers/plugable-record';
import { FeatureContext, createFeature } from './feature';
import type { StylableMeta } from '../stylable-meta';
import * as STSymbol from './st-symbol';
import * as STCustomSelector from './st-custom-selector';
import * as STCustomState from './st-custom-state';
import type { MappedStates } from './st-custom-state';
import * as CSSClass from './css-class';
import { warnOnce } from '../helpers/deprecation';
import postcss from 'postcss';
import { parseCSSValue, stringifyCSSValue, BaseAstNode } from '@tokey/css-value-parser';
import { parseSelectorWithCache, walkSelector } from '../helpers/selector';
import {
ImmutableSelector,
stringifySelectorAst,
ImmutablePseudoClass,
ImmutableSelectorList,
} from '@tokey/css-selector-parser';
import { createDiagnosticReporter } from '../diagnostics';
import { getAlias } from '../stylable-utils';
import {
findAnything,
findFatArrow,
findNextClassNode,
findNextPseudoClassNode,
findPseudoElementNode,
isExactLiteral,
} from '../helpers/css-value-seeker';
export const diagnostics = {
GLOBAL_MAPPING_LIMITATION: createDiagnosticReporter(
'21000',
'error',
() => `Currently class mapping is limited to single global selector: :global(<selector>)`
),
UNSUPPORTED_TOP_DEF: createDiagnosticReporter(
'21001',
'error',
() => 'top level @st must start with a class'
),
MISSING_EXTEND: createDiagnosticReporter(
'21002',
'error',
() => `missing required class reference to extend a class (e.g. ":is(.class-name)"`
),
OVERRIDE_IMPORTED_CLASS: createDiagnosticReporter(
'21003',
'error',
() => `cannot override imported class definition`
),
STATE_OUT_OF_CONTEXT: createDiagnosticReporter(
'21004',
'error',
() => 'pseudo-state definition must be directly nested in a `@st .class{}` definition'
),
// 31005 - unused
MISSING_MAPPED_SELECTOR: createDiagnosticReporter(
'21006',
'error',
() => `missing mapped selector after "=>"`
),
MULTI_MAPPED_SELECTOR: createDiagnosticReporter(
'21007',
'error',
() =>
'mapped selector accepts only a single selector.\nuse `:is()` or `:where()` to map multiple selectors)'
),
ELEMENT_OUT_OF_CONTEXT: createDiagnosticReporter(
'21008',
'error',
() => 'pseudo-element definition must be directly nested in a `@st .class{}` definition'
),
MISSING_MAPPING: createDiagnosticReporter(
'21009',
'error',
() => 'expected selector mapping (e.g. "=> <selector>")'
),
REDECLARE: createDiagnosticReporter(
'21010',
'error',
(type: string, src: string) => `redeclare ${type} definition: "${src}"`
),
INVALID_ST_DEF: createDiagnosticReporter(
'21011',
'error',
(params: string) => `invalid "${params}" definition`
),
MAPPING_UNSUPPORTED_NESTING: createDiagnosticReporter(
'21012',
'error',
() => 'mapped selector can only contain `&` as an initial selector'
),
UNEXPECTED_EXTRA_VALUE: createDiagnosticReporter(
'21013',
'error',
(extraValue: string) => `found unexpected extra value definition: "${extraValue}"`
),
CLASS_OUT_OF_CONTEXT: createDiagnosticReporter(
'21014',
'error',
() => 'class definition must be top level'
),
};
export interface PartSymbol extends HasParts, STCustomState.HasStates {
_kind: 'part';
name: string;
id: string;
mapTo: ImmutableSelectorList | CSSClass.ClassSymbol;
}
export interface HasParts {
'-st-parts': Record<string, PartSymbol>;
}
export const experimentalMsg = '[experimental feature] stylable structure (@st): API might change!';
const dataKey = plugableRecord.key<{
isStructureMode: boolean;
analyzedDefs: WeakMap<postcss.AtRule, AnalyzedStDef>;
analyzedDefToPartSymbol: Map<AnalyzedStDef, PartSymbol>;
declaredClasses: Set<string>;
}>('st-structure');
// HOOKS
export const hooks = createFeature({
analyzeInit(context) {
const { meta } = context;
if (meta.type !== 'stylable') {
return;
}
const stAtRule = meta.sourceAst.nodes.find((node) => isStAtRule(node));
if (stAtRule) {
warnOnce(experimentalMsg);
const metaAnalysis = plugableRecord.getUnsafe(context.meta.data, dataKey);
metaAnalysis.isStructureMode = true;
} else {
// set implicit root for legacy mode (root with flat structure)
meta.root = 'root';
const rootSymbol = CSSClass.addClass(context, 'root');
rootSymbol[`-st-root`] = true;
}
},
metaInit({ meta }) {
plugableRecord.set(meta.data, dataKey, {
// default to legacy flat mode
isStructureMode: false,
analyzedDefs: new WeakMap(),
analyzedDefToPartSymbol: new Map(),
declaredClasses: new Set<string>(),
});
},
analyzeAtRule({ context, atRule, analyzeRule }) {
if (!isStAtRule(atRule) || context.meta.type !== 'stylable') {
return;
}
const { analyzedDefToPartSymbol, declaredClasses } = plugableRecord.getUnsafe(
context.meta.data,
dataKey
);
const analyzed = analyzeStAtRule(atRule, context);
if (!analyzed) {
// not valid
} else if (analyzed.type === 'topLevelClass') {
declaredClasses.add(analyzed.name);
CSSClass.addClass(context, analyzed.name, atRule);
CSSClass.disableDirectivesForClass(context, analyzed.name);
// extend class
if (analyzed.extendedClass) {
const extendedSymbol =
CSSClass.get(context.meta, analyzed.extendedClass) ||
CSSClass.addClass(context, analyzed.extendedClass);
CSSClass.extendTypedRule(
context,
atRule,
'.' + analyzed.name,
'-st-extends',
getAlias(extendedSymbol) || extendedSymbol
);
}
// class mapping
if (analyzed.mappedSelector) {
// ToDo: support non global mapping
const globalNode = findGlobalPseudo(analyzed);
if (globalNode && globalNode.nodes?.length === 1) {
const mappedSelectorAst = globalNode.nodes[0];
// analyze mapped selector
analyzeRule(
postcss.rule({
selector: stringifySelectorAst(mappedSelectorAst),
source: atRule.source,
}),
{
isScoped: false,
originalNode: atRule,
}
);
// register global mapping to class
CSSClass.extendTypedRule(
context,
atRule,
analyzed.name,
'-st-global',
mappedSelectorAst.nodes
);
}
}
} else if (analyzed.type === 'part') {
const parentSymbol = getPartParentSymbol(context, analyzed, analyzedDefToPartSymbol);
if (!parentSymbol) {
// unreachable: assuming analyzing @st definitions dfs - class/part must be defined
context.diagnostics.report(diagnostics.ELEMENT_OUT_OF_CONTEXT(), {
node: atRule,
});
return;
}
const partName = analyzed.name;
// check re-declare
if (getPart(parentSymbol, partName)) {
const srcWord = '::' + partName;
context.diagnostics.report(diagnostics.REDECLARE('pseudo-element', srcWord), {
node: atRule,
word: srcWord,
});
return;
}
// analyze mapped selector
analyzeRule(
postcss.rule({
selector: stringifySelectorAst(analyzed.mappedSelector),
source: atRule.source,
}),
{
isScoped: true,
originalNode: atRule,
}
);
// register part mapping to parent definition
const partSymbol = setPart(parentSymbol, getSymbolId(parentSymbol), partName, [
analyzed.mappedSelector,
]);
analyzedDefToPartSymbol.set(analyzed, partSymbol);
} else if (analyzed.type === 'state') {
const parentSymbol =
analyzed.parentAnalyze.type === 'topLevelClass'
? CSSClass.get(context.meta, analyzed.parentAnalyze.name)
: analyzedDefToPartSymbol.get(analyzed.parentAnalyze);
if (!parentSymbol) {
// unreachable: assuming analyzing @st definitions dfs - class/part must be defined
context.diagnostics.report(diagnostics.STATE_OUT_OF_CONTEXT(), {
node: atRule,
});
return;
}
const mappedStates = (parentSymbol['-st-states'] ||= {});
const stateName = analyzed.name;
if (mappedStates[stateName]) {
// first state definition wins
const srcWord = ':' + stateName;
context.diagnostics.report(diagnostics.REDECLARE('pseudo-state', srcWord), {
node: atRule,
word: srcWord,
});
return;
}
mappedStates[stateName] = analyzed.stateDef;
}
},
analyzeDone({ meta }) {
const { isStructureMode } = plugableRecord.getUnsafe(meta.data, dataKey);
if (meta.type === 'stylable' && !isStructureMode) {
// legacy flat mode:
// classes and custom-selectors are registered as .root pseudo-elements
const customSelectors = STCustomSelector.getCustomSelectors(meta);
const classes = CSSClass.getAll(meta);
const rootClass = classes['root'];
const rootId = getSymbolId(rootClass);
// custom-selector definition precedence over class definition
for (const [partName, mapTo] of Object.entries(customSelectors)) {
setPart(rootClass, rootId, partName, mapTo);
}
for (const [className, classSymbol] of Object.entries(classes)) {
if (className === 'root' || customSelectors[className]) {
continue;
}
setPart(rootClass, rootId, className, classSymbol);
}
}
},
});
// API
function isStAtRule(node: postcss.AnyNode): node is postcss.AtRule {
return node?.type === 'atrule' && node.name === 'st';
}
function getPartParentSymbol(
context: FeatureContext,
{ parentAnalyze }: ParsedStPart,
analyzedDefToPartSymbol: Map<AnalyzedStDef, PartSymbol>
) {
return parentAnalyze.type === 'topLevelClass'
? CSSClass.get(context.meta, parentAnalyze.name)
: analyzedDefToPartSymbol.get(parentAnalyze);
}
export function isStructureMode(meta: StylableMeta) {
return plugableRecord.getUnsafe(meta.data, dataKey).isStructureMode;
}
export function createPartSymbol(
input: Partial<PartSymbol> & Pick<PartSymbol, 'name' | 'id' | 'mapTo'>
): PartSymbol {
const parts = input['-st-parts'] || {};
const states = input['-st-states'] || {};
return { ...input, _kind: 'part', '-st-parts': parts, '-st-states': states };
}
export function setPart(
symbol: HasParts,
parentId: string,
partName: string,
mapTo: PartSymbol['mapTo']
) {
const partSymbol = createPartSymbol({
name: partName,
id: parentId + '::' + partName,
mapTo,
});
symbol['-st-parts'][partName] = partSymbol;
return partSymbol;
}
export function getParts(symbol: HasParts) {
return symbol['-st-parts'];
}
export function getPart(symbol: HasParts, name: string): PartSymbol | undefined {
return symbol['-st-parts'][name];
}
export function getPartNames(symbol: HasParts) {
return Object.keys(symbol['-st-parts']);
}
function getSymbolId(symbol: CSSClass.ClassSymbol | PartSymbol) {
return symbol._kind === 'class' ? '.' + symbol.name : symbol.id;
}
type ParsedStClass = {
type: 'topLevelClass';
params: BaseAstNode[];
match: boolean;
ranges: Record<'class' | 'extend' | 'mapArrow' | 'mapTo' | 'leftoverValue', BaseAstNode[]>;
name: string;
extendedClass?: string;
mappedSelector?: ImmutableSelector;
};
interface ParsedStPart {
type: 'part';
params: BaseAstNode[];
match: boolean;
ranges: Record<'pseudoElement' | 'mapArrow' | 'mapTo' | 'leftoverValue', BaseAstNode[]>;
name: string;
parentAnalyze: ParsedStClass | ParsedStPart;
mappedArrow: boolean;
mappedSelector: ImmutableSelector;
}
interface ParsedStState {
type: 'state';
params: BaseAstNode[];
match: boolean;
ranges: Record<'leftoverValue', BaseAstNode[]>;
name: string;
parentAnalyze: ParsedStClass | ParsedStPart;
stateDef: MappedStates[string];
}
function isMatch(result: any): result is AnalyzedStDef {
return result.match;
}
type AnalyzedStDef = ParsedStClass | ParsedStPart | ParsedStState;
function analyzeStAtRule(
atRule: postcss.AtRule,
context: FeatureContext
): AnalyzedStDef | undefined {
// cache
const { analyzedDefs } = plugableRecord.getUnsafe(context.meta.data, dataKey);
if (analyzedDefs.has(atRule)) {
return analyzedDefs.get(atRule);
}
// parse
const params = parseCSSValue(atRuleFullParams(atRule));
const def =
params.length === 0
? undefined
: parseClassDefinition(atRule, params) ||
parsePseudoElementDefinition(context, atRule, params) ||
parseStateDefinition(context, atRule, params);
if (!def) {
if (atRule.parent?.type === 'root') {
context.diagnostics.report(diagnostics.UNSUPPORTED_TOP_DEF(), {
node: atRule,
});
} else {
context.diagnostics.report(diagnostics.INVALID_ST_DEF(atRule.params), {
node: atRule,
});
}
return;
}
// validate
switch (def.type) {
case 'topLevelClass': {
if (!validateTopLevelClass({ def, atRule, context })) {
return;
}
break;
}
case 'part': {
if (!validatePart({ def, atRule, context })) {
return;
}
break;
}
case 'state': {
if (!validateState({ def, atRule, context })) {
return;
}
break;
}
}
if (!isMatch(def)) {
return;
}
analyzedDefs.set(atRule, def);
return def;
}
function validateTopLevelClass({
def,
atRule,
context,
}: {
def: Partial<ParsedStClass>;
atRule: postcss.AtRule;
context: FeatureContext;
}) {
const { declaredClasses } = plugableRecord.getUnsafe(context.meta.data, dataKey);
if (!def.ranges || !def.params) {
// should always be provided by parser
return false;
}
if (!def.name) {
// ToDo: fix type to have name required
return false;
}
if (def.ranges.extend.length && !def.extendedClass) {
context.diagnostics.report(diagnostics.MISSING_EXTEND(), {
node: atRule,
word: stringifyCSSValue(def.ranges.extend),
});
}
if (def.ranges.leftoverValue.find((node) => node.type !== 'comment' && node.type !== 'space')) {
const unexpectedValue = stringifyCSSValue(def.ranges.leftoverValue).trim();
context.diagnostics.report(diagnostics.UNEXPECTED_EXTRA_VALUE(unexpectedValue), {
node: atRule,
word: unexpectedValue,
});
return false;
}
if (atRule.parent?.type !== 'root') {
context.diagnostics.report(diagnostics.CLASS_OUT_OF_CONTEXT(), {
node: atRule,
});
return false;
}
const existingSymbol = STSymbol.get(context.meta, def.name);
if (existingSymbol?._kind === 'import') {
context.diagnostics.report(diagnostics.OVERRIDE_IMPORTED_CLASS(), {
node: atRule,
});
return false;
}
if (declaredClasses.has(def.name)) {
// ToDo: use st-symbol redeclare api; improve st-symbol/css-class "final" marking and diagnostics
const srcWord = '.' + def.name;
context.diagnostics.report(diagnostics.REDECLARE('class', srcWord), {
node: atRule,
word: srcWord,
});
return false;
}
if (def.ranges.mapArrow.length) {
if (!def.mappedSelector) {
// report missing selector
const arrowEnd = def.ranges.mapArrow[def.ranges.mapArrow.length - 1];
for (let i = def.params.length - 1; i >= 0; i--) {
const node: BaseAstNode = def.params[i];
if (node === arrowEnd) {
break;
} else if (isExactLiteral(node, ',')) {
context.diagnostics.report(diagnostics.MULTI_MAPPED_SELECTOR(), {
node: atRule,
});
return false;
}
}
context.diagnostics.report(diagnostics.MISSING_MAPPED_SELECTOR(), {
node: atRule,
});
return false;
}
const globalNode = findGlobalPseudo(def, true);
if (!globalNode || !globalNode.nodes || globalNode.nodes.length !== 1) {
context.diagnostics.report(diagnostics.GLOBAL_MAPPING_LIMITATION(), {
node: atRule,
word: stringifySelectorAst(def.mappedSelector).trim(),
});
return false;
}
}
return true;
}
function validatePart({
def,
atRule,
context,
}: {
def: Partial<ParsedStPart>;
atRule: postcss.AtRule;
context: FeatureContext;
}) {
if (!def.parentAnalyze) {
context.diagnostics.report(diagnostics.ELEMENT_OUT_OF_CONTEXT(), {
node: atRule,
});
return false;
}
if (!def.mappedSelector) {
if (!def.mappedArrow) {
context.diagnostics.report(diagnostics.MISSING_MAPPING(), {
node: atRule,
});
return false;
}
// report missing selector
const arrowEnd = def.ranges!.mapArrow[def.ranges!.mapArrow.length - 1];
for (let i = def.params!.length - 1; i >= 0; i--) {
const node = def.params![i];
if (node === arrowEnd) {
break;
} else if (isExactLiteral(node, ',')) {
context.diagnostics.report(diagnostics.MULTI_MAPPED_SELECTOR(), {
node: atRule,
});
return false;
}
}
context.diagnostics.report(diagnostics.MISSING_MAPPED_SELECTOR(), {
node: atRule,
});
return false;
}
if (validateNestingInMapping(def.mappedSelector, context, atRule)) {
return false;
}
return true;
}
function validateState({
def,
atRule,
context,
}: {
def: Partial<ParsedStState>;
atRule: postcss.AtRule;
context: FeatureContext;
}) {
if (!def.parentAnalyze) {
context.diagnostics.report(diagnostics.STATE_OUT_OF_CONTEXT(), {
node: atRule,
});
return false;
}
const [amountToActualValue] = findAnything(def.ranges!.leftoverValue, 0);
if (amountToActualValue) {
const unexpectedValue = stringifyCSSValue(def.ranges!.leftoverValue).trim();
context.diagnostics.report(diagnostics.UNEXPECTED_EXTRA_VALUE(unexpectedValue), {
node: atRule,
word: unexpectedValue,
});
return false;
}
return true;
}
function findGlobalPseudo(def: Partial<ParsedStClass>, checkAfter = false) {
if (!def.mappedSelector) {
return;
}
let globalNode: ImmutablePseudoClass | undefined = undefined;
let foundUnexpectedSelector = false;
for (const node of def.mappedSelector.nodes) {
if (node.type === 'pseudo_class' && node.value === 'global' && !globalNode) {
globalNode = node;
if (!checkAfter) {
break;
}
} else if (node.type !== 'comment') {
foundUnexpectedSelector = true;
}
}
return foundUnexpectedSelector ? undefined : globalNode;
}
function parseStateDefinition(
context: FeatureContext,
atRule: postcss.AtRule,
params: BaseAstNode[]
) {
const result: Partial<ParsedStState> = {
type: 'state',
params,
match: true,
ranges: { leftoverValue: [] },
};
let index = 0;
const { analyzedDefs } = plugableRecord.getUnsafe(context.meta.data, dataKey); // name
const [amountToName, nameNode] = findNextPseudoClassNode(params, 0);
if (nameNode) {
result.name = nameNode.value;
} else {
// not a pseudo-state definition
return;
}
index += amountToName;
// parent
const parentRule = atRule.parent;
const parentAnalyze = parentRule && analyzedDefs.get(parentRule as any);
if (
parentAnalyze &&
(parentAnalyze.type === 'topLevelClass' || parentAnalyze.type === 'part')
) {
result.parentAnalyze = parentAnalyze;
}
// state
const [amountToStateDef, stateDef] = STCustomState.parseStateValue(
params.slice(index - 1),
atRule,
context.diagnostics
);
if (stateDef !== undefined) {
index += amountToStateDef;
result.stateDef = stateDef;
} else {
result.match = false;
}
// leftover
const amountTaken = index - 1;
result.ranges!.leftoverValue.push(...params.slice(amountTaken));
const [amountToUnexpected] = findAnything(params, index);
if (amountToUnexpected) {
result.match = false;
}
return result;
}
function parsePseudoElementDefinition(
context: FeatureContext,
atRule: postcss.AtRule,
params: BaseAstNode[]
) {
const { analyzedDefs } = plugableRecord.getUnsafe(context.meta.data, dataKey);
const result: Partial<ParsedStPart> = {
type: 'part',
params,
match: true,
name: '',
ranges: { pseudoElement: [], mapArrow: [], mapTo: [], leftoverValue: [] },
};
let index = 0;
// collect pseudo element name
const [amountToName, nameNode, nameInspectAmount] = findPseudoElementNode(params, 0);
result.ranges!.pseudoElement.push(...params.slice(index, index + nameInspectAmount));
index += amountToName;
if (nameNode) {
result.name = nameNode.value;
} else {
// not a pseudo-element definition
return false;
}
// get symbol to extend
const parentRule = atRule.parent;
const parentAnalyze = parentRule && analyzedDefs.get(parentRule as any);
if (parentAnalyze?.type === 'topLevelClass' || parentAnalyze?.type === 'part') {
result.parentAnalyze = parentAnalyze;
}
// collect mapped selector
const [amountToMapping, mappingOpenNode, mapArrowInspectAmount] = findFatArrow(params, index, {
stopOnFail: false,
});
result.ranges!.mapArrow.push(...params.slice(index, index + mapArrowInspectAmount));
index += amountToMapping;
if (mappingOpenNode) {
result.mappedArrow = true;
// selector
result.ranges!.mapTo.push(...params.slice(index));
index = params.length - 1;
const selectorStr = atRuleFullParams(atRule).slice(mappingOpenNode.end);
const mappedSelectors = parseSelectorWithCache(selectorStr.trim());
const filteredSelector =
mappedSelectors.length === 1 && filterCommentsAndSpaces(mappedSelectors[0]);
if (filteredSelector && filteredSelector.nodes.length) {
result.mappedSelector = filteredSelector;
}
} else {
result.match = false;
}
// check unexpected extra
result.ranges!.leftoverValue.push(...params.slice(index + 1));
return result;
}
function parseClassDefinition(atRule: postcss.AtRule, params: BaseAstNode[]) {
const result: ParsedStClass = {
type: 'topLevelClass',
params,
match: true,
ranges: { class: [], extend: [], mapArrow: [], mapTo: [], leftoverValue: [] },
name: '',
};
let index = 0;
// top level class
const [amountToClass, classNameNode, classInspectedAmount] = findNextClassNode(params, index, {
stopOnFail: true,
});
result.ranges.class.push(...params.slice(index, index + classInspectedAmount));
index += amountToClass;
if (classNameNode) {
result.name = classNameNode.value;
} else {
// not a class definition
return false;
}
// collect extends class
const [amountToExtends, extendsNode, extendInspectAmount] = findNextPseudoClassNode(
params,
index,
{
name: 'is',
stopOnFail: true,
stopOnMatch: (_node, index, nodes) => {
const [amountToFatArrow] = findFatArrow(nodes, index, { stopOnFail: true });
return amountToFatArrow > 0;
},
}
);
if (extendsNode) {
result.ranges.extend.push(...params.slice(index, index + extendInspectAmount));
index += amountToExtends;
if (extendsNode.type === 'call') {
const [amountToExtendedClass, nameNode] = findNextClassNode(extendsNode.args, 0, {
stopOnFail: true,
});
if (amountToExtendedClass) {
index += amountToExtends;
// check leftover nodes
const [amountToUnexpectedNode] = findAnything(
extendsNode.args,
amountToExtendedClass
);
if (!amountToUnexpectedNode) {
result.extendedClass = nameNode!.value;
}
}
}
}
// collect mapped selector
const [amountToMapping, mappingOpenNode, mapArrowInspectAmount] = findFatArrow(params, index, {
stopOnFail: false,
});
if (mappingOpenNode) {
result.ranges.mapArrow.push(...params.slice(index, index + mapArrowInspectAmount));
index += amountToMapping;
// selector
result.ranges.mapTo.push(...params.slice(index));
index = params.length;
const selectorStr = atRuleFullParams(atRule).slice(mappingOpenNode.end);
const mappedSelectors = parseSelectorWithCache(selectorStr);
const filteredSelector =
mappedSelectors.length === 1 && filterCommentsAndSpaces(mappedSelectors[0]);
if (filteredSelector && filteredSelector.nodes.length) {
result.mappedSelector = filteredSelector;
}
}
// unexpected extra value
result.ranges.leftoverValue.push(...params.slice(index));
return result;
}
function validateNestingInMapping(
selector: ImmutableSelector,
context: FeatureContext,
atRule: postcss.AtRule
) {
// check for unsupported & anywhere except first
let invalid = false;
let passedActualSelector = false;
walkSelector(selector, (node) => {
if (passedActualSelector && node.type === 'nesting') {
context.diagnostics.report(diagnostics.MAPPING_UNSUPPORTED_NESTING(), {
node: atRule,
});
invalid = true;
return walkSelector.stopAll;
} else if (node.type !== 'comment' && node.type !== 'selector') {
passedActualSelector = true;
}
return;
});
return invalid;
}
function atRuleFullParams(atRule: postcss.AtRule) {
const afterName = atRule.raws.afterName || '';
const between = atRule.raws.between || '';
return afterName + atRule.params + between;
}
function filterCommentsAndSpaces(selector: ImmutableSelector) {
const filteredSelector: ImmutableSelector = {
...selector,
after: '',
before: '',
nodes: selector.nodes.filter((node) => node.type !== 'comment'),
};
return filteredSelector;
}