import type * as postcss from 'postcss'; import postcssValueParser, { type Node as ValueNode, type FunctionNode, } from 'postcss-value-parser'; import cssesc from 'cssesc'; import type { PseudoClass, SelectorList, SelectorNode } from '@tokey/css-selector-parser'; import { createDiagnosticReporter, Diagnostics } from '../diagnostics'; import { parseSelectorWithCache, stringifySelector, convertToClass, convertToInvalid, convertToSelector, } from './selector'; import { groupValues, listOptions } from './value'; import { stripQuotation } from './string'; import { evalDeclarationValue } from '../functions'; import type { StylableMeta } from '../stylable-meta'; import type { StylableResolver } from '../stylable-resolver'; import type { ParsedValue } from '../types'; import { CSSClass } from '../features'; import { reservedFunctionalPseudoClasses } from '../native-reserved-lists'; import { BaseAstNode, stringifyCSSValue } from '@tokey/css-value-parser'; import { findCustomIdent, findNextCallNode } from './css-value-seeker'; export interface MappedStates { [s: string]: StateParsedValue | string | TemplateStateParsedValue | null; } export interface TemplateStateParsedValue { type: 'template'; template: string; params: [StateParsedValue]; } export interface StateParsedValue { type: string; defaultValue?: string; arguments: StateArguments; } export interface StateTypeValidator { name: string; args: string[]; } type StateArguments = Array; export const stateMiddleDelimiter = '-'; export const booleanStateDelimiter = '--'; export const stateWithParamDelimiter = booleanStateDelimiter + stateMiddleDelimiter; export const stateDiagnostics = { MISSING_TYPE_OR_TEMPLATE: createDiagnosticReporter( '08000', 'error', (name: string) => `pseudo-state "${name}" missing type or template` ), UNKNOWN_STATE_TYPE: createDiagnosticReporter( '08002', 'error', (name: string, type: string) => `pseudo-state "${name}" defined with unknown type: "${type}"` ), TOO_MANY_STATE_TYPES: createDiagnosticReporter( '08003', 'error', (name: string, types: string[]) => `pseudo-state "${name}(${types.join(', ')})" definition must be of a single type` ), NO_STATE_TYPE_GIVEN: createDiagnosticReporter( '08005', 'warning', (name: string) => `pseudo-state "${name}" expected a definition of a single type, but received none` ), TOO_MANY_ARGS_IN_VALIDATOR: createDiagnosticReporter( '08006', 'error', (name: string, validator: string, args: string[]) => `pseudo-state "${name}" expected "${validator}" validator to receive a single argument, but it received "${args.join( ', ' )}"` ), STATE_STARTS_WITH_HYPHEN: createDiagnosticReporter( '08007', 'error', (name: string) => `state "${name}" declaration cannot begin with a "${stateMiddleDelimiter}" character` ), RESERVED_NATIVE_STATE: createDiagnosticReporter( '08008', 'warning', (name: string) => `state "${name}" is reserved for native pseudo-class` ), DEFAULT_PARAM_FAILS_VALIDATION: createDiagnosticReporter( '08010', 'error', (stateName: string, defaultValue: string, errors: string[]) => `pseudo-state "${stateName}" default value "${defaultValue}" failed validation:\n${errors.join( '\n' )}` ), NO_STATE_ARGUMENT_GIVEN: createDiagnosticReporter( '08004', 'error', (name: string, type: string) => `pseudo-state "${name}" expected argument of type "${type}" but got none` ), FAILED_STATE_VALIDATION: createDiagnosticReporter( '08009', 'error', (name: string, actualParam: string, errors: string[]) => [ `pseudo-state "${name}" with parameter "${actualParam}" failed validation:`, ...errors, ].join('\n') ), TEMPLATE_MISSING_PLACEHOLDER: createDiagnosticReporter( '08011', 'warning', (state: string, template: string) => `pseudo-state "${state}" template "${template}" is missing a placeholder, use "$0" to set the parameter insertion place` ), TEMPLATE_MULTI_PARAMETERS: createDiagnosticReporter( '08012', 'error', (state: string) => `pseudo-state "${state}" template only supports a single parameter` ), TEMPLATE_MISSING_PARAMETER: createDiagnosticReporter( '08013', 'error', (state: string) => `pseudo-state "${state}" template expected a parameter definition` ), UNSUPPORTED_MULTI_SELECTOR: createDiagnosticReporter( '08014', 'error', (state: string, finalSelector: string) => `pseudo-state "${state}" resulted in an unsupported multi selector "${finalSelector}"` ), UNSUPPORTED_COMPLEX_SELECTOR: createDiagnosticReporter( '08015', 'error', (state: string, finalSelector: string) => `pseudo-state "${state}" resulted in an unsupported complex selector "${finalSelector}"` ), INVALID_SELECTOR: createDiagnosticReporter( '08016', 'error', (state: string, finalSelector: string) => `pseudo-state "${state}" resulted in an invalid selector "${finalSelector}"` ), UNSUPPORTED_INITIAL_SELECTOR: createDiagnosticReporter( '08017', 'error', (state: string, finalSelector: string) => `pseudo-state "${state}" result cannot start with a type or universal selector "${finalSelector}"` ), NO_PARAM_REQUIRED: createDiagnosticReporter( '08018', 'error', (name: string, param: string) => `pseudo-state "${name}" accepts no parameter, but received "${param}"` ), }; // parse export function parsePseudoStates( value: string, decl: postcss.Declaration, diagnostics: Diagnostics ) { const mappedStates: MappedStates = {}; const ast = postcssValueParser(value); const statesSplitByComma = groupValues(ast.nodes); statesSplitByComma.forEach((workingState: ParsedValue[]) => { const [stateDefinition, ...stateDefault] = workingState; const stateName = stateDefinition.value; if (!validateStateName(stateName, diagnostics, decl)) { return; } if (stateDefinition.type === 'function') { resolveStateType( stateDefinition as FunctionNode, mappedStates, stateDefault, diagnostics, decl ); } else if (stateDefinition.type === 'word') { resolveBooleanState(mappedStates, stateDefinition); } else { // TODO: Invalid state, edge case needs warning } }); return mappedStates; } function validateStateName(name: string, diagnostics: Diagnostics, node: postcss.Node) { if (name.startsWith('-')) { diagnostics.report(stateDiagnostics.STATE_STARTS_WITH_HYPHEN(name), { node: node, word: name, }); } else if (reservedFunctionalPseudoClasses.includes(name)) { diagnostics.report(stateDiagnostics.RESERVED_NATIVE_STATE(name), { node: node, word: name, }); return false; } return true; } export function parseStateValue( value: BaseAstNode[], node: postcss.Node, diagnostics: Diagnostics ): [amountTaken: number, stateDef: MappedStates[string] | undefined] { let stateName = ''; let stateDef: MappedStates[string] = null; /*boolean*/ let amountTaken = 0; const customIdentResult = findCustomIdent(value, 0); const [amountToName, nameNode] = customIdentResult[0] ? customIdentResult : findNextCallNode(value, 0); if (nameNode && validateStateName(nameNode.value, diagnostics, node)) { amountTaken += amountToName; stateName = nameNode.value; // state with parameter if (nameNode.type === 'call') { // take all of the definition since default value takes the rest amountTaken = value.length; // ToDo: translate resolveStateType to tokey and remove the double parsing const postcssStateValue = postcssValueParser( stringifyCSSValue(value.slice(amountToName - 1)) ); // get state definition const [stateDefinition, ...stateDefault] = postcssStateValue.nodes; const stateMap: MappedStates = {}; resolveStateType( stateDefinition as FunctionNode, stateMap, stateDefault, diagnostics, node as postcss.Declaration // ToDo: change to accept any postcss node ); if (stateMap[stateName]) { stateDef = stateMap[stateName]; } } } if (stateName) { return [amountTaken, stateDef]; } return [0, undefined]; } function resolveBooleanState(mappedStates: MappedStates, stateDefinition: ParsedValue) { const currentState = mappedStates[stateDefinition.value]; if (!currentState) { mappedStates[stateDefinition.value] = null; // add boolean state } else { // TODO: warn with such name already exists } } function resolveStateType( stateDefinition: FunctionNode, mappedStates: MappedStates, stateDefault: ParsedValue[], diagnostics: Diagnostics, decl: postcss.Declaration ) { const stateName = stateDefinition.value; if (stateDefinition.nodes.length === 0) { resolveBooleanState(mappedStates, stateDefinition); diagnostics.report(stateDiagnostics.NO_STATE_TYPE_GIVEN(stateName), { node: decl, word: decl.value, }); return; } const { paramType, argsFirstNode, argsFullValue } = collectStateArgsDef(stateDefinition.nodes); if (!paramType) { diagnostics.report(stateDiagnostics.MISSING_TYPE_OR_TEMPLATE(stateName), { node: decl, }); return; } if (paramType?.type === 'string') { defineTemplateState( stateName, paramType, argsFirstNode, argsFullValue, mappedStates, diagnostics, decl ); } else { if (argsFullValue.length > 1) { diagnostics.report( stateDiagnostics.TOO_MANY_STATE_TYPES( stateName, argsFirstNode.map((argNode) => argNode ? postcssValueParser.stringify(argNode) : '' ) ), { node: decl, word: decl.value, } ); } defineParamState( stateName, paramType, stateDefault, mappedStates, diagnostics, stateDefinition, decl ); } } function defineTemplateState( stateName: string, templateDef: postcssValueParser.StringNode, argsFirstNode: (postcssValueParser.Node | undefined)[], argsFullValue: postcssValueParser.Node[][], mappedStates: MappedStates, diagnostics: Diagnostics, decl: postcss.Declaration ) { const template = stripQuotation(postcssValueParser.stringify(templateDef)); if (argsFullValue.length === 1) { // simple template with no params const selectorStr = template.trim().replace(/\\["']/g, '"'); const selectorAst = parseSelectorWithCache(selectorStr, { clone: true }); if ( !validateTemplateSelector({ stateName, selectorStr, selectorAst, cssNode: decl, diagnostics, }) ) { return; } else { mappedStates[stateName] = selectorStr; } } else if (argsFullValue.length === 2) { // single parameter template if (!template.includes('$0')) { diagnostics.report(stateDiagnostics.TEMPLATE_MISSING_PLACEHOLDER(stateName, template), { node: decl, word: template, }); } const paramFullDef = argsFullValue[1]; const paramTypeDef = argsFirstNode[1]; if (!paramTypeDef) { diagnostics.report(stateDiagnostics.TEMPLATE_MISSING_PARAMETER(stateName), { node: decl, }); return; } const param = createStateParamDef( stateName + ' parameter', paramTypeDef, paramFullDef.splice(paramFullDef.indexOf(paramTypeDef) + 1), diagnostics, decl ); if (!param) { // UNKNOWN_STATE_TYPE reported in createStateParamDef return; } const templateStateType: TemplateStateParsedValue = { type: 'template', template, params: [param], }; mappedStates[stateName] = templateStateType; } else { // unsupported multiple params diagnostics.report(stateDiagnostics.TEMPLATE_MULTI_PARAMETERS(stateName), { node: decl, }); } } function defineParamState( stateName: string, paramType: postcssValueParser.Node, stateDefault: ParsedValue[], mappedStates: MappedStates, diagnostics: Diagnostics, stateDefinition: FunctionNode, decl: postcss.Declaration ) { if (paramType.value === 'boolean') { // explicit boolean // ToDo: remove support resolveBooleanState(mappedStates, stateDefinition); } else { const stateParamDef = createStateParamDef( stateName, paramType, stateDefault, diagnostics, decl ); if (stateParamDef) { mappedStates[stateName] = stateParamDef; } } } function createStateParamDef( stateName: string, typeDef: postcssValueParser.Node, stateDefault: ParsedValue[], diagnostics: Diagnostics, decl: postcss.Declaration ): StateParsedValue | undefined { const type = typeDef.value; if (type in systemValidators && (typeDef.type === 'function' || typeDef.type === 'word')) { const stateType: StateParsedValue = { type, arguments: [], defaultValue: postcssValueParser .stringify(stateDefault as postcssValueParser.Node[]) .trim(), }; if (typeDef.type === 'function' && typeDef.nodes.length > 0) { resolveArguments(typeDef, stateType, stateName, diagnostics, decl); } return stateType; } else { const srcValue = postcssValueParser.stringify(typeDef); diagnostics.report(stateDiagnostics.UNKNOWN_STATE_TYPE(stateName, srcValue), { node: decl, word: srcValue, }); return; } } function collectStateArgsDef(nodes: ValueNode[]) { const argsFullValue: ValueNode[][] = []; const argsFirstNode: Array = []; let collectedArg: ValueNode[] = []; let firstActualValue: ValueNode | undefined = undefined; for (const node of nodes) { if (node.type === 'div') { argsFullValue.push(collectedArg); argsFirstNode.push(firstActualValue); collectedArg = []; firstActualValue = undefined; } else { collectedArg.push(node); if (!firstActualValue && node.type !== 'space' && node.type !== 'comment') { firstActualValue = node; } } } if (collectedArg.length) { argsFullValue.push(collectedArg); argsFirstNode.push(firstActualValue); } const paramType = argsFirstNode[0]; return { paramType, argsFullValue, argsFirstNode }; } function resolveArguments( paramType: ParsedValue, stateType: StateParsedValue, name: string, diagnostics: Diagnostics, decl: postcss.Declaration ) { const separatedByComma = groupValues(paramType.nodes); separatedByComma.forEach((group) => { const validator = group[0]; if (validator.type === 'function') { const args = listOptions(validator); if (args.length > 1) { diagnostics.report( stateDiagnostics.TOO_MANY_ARGS_IN_VALIDATOR(name, validator.value, args), { node: decl, word: decl.value, } ); } else { stateType.arguments.push({ name: validator.value, args, }); } } else if (validator.type === 'string' || validator.type === 'word') { stateType.arguments.push(validator.value); } }); } // validation export interface StateResult { res: string; errors: string[] | null; } export const validationErrors = { string: { STRING_TYPE_VALIDATION_FAILED: (actualParam: string) => `"${actualParam}" should be of type string`, REGEX_VALIDATION_FAILED: (regex: string, actualParam: string) => `expected "${actualParam}" to match regex "${regex}"`, CONTAINS_VALIDATION_FAILED: (shouldContain: string, actualParam: string) => `expected "${actualParam}" to contain string "${shouldContain}"`, MIN_LENGTH_VALIDATION_FAILED: (length: string, actualParam: string) => `expected "${actualParam}" to be of length longer than or equal to ${length}`, MAX_LENGTH_VALIDATION_FAILED: (length: string, actualParam: string) => `expected "${actualParam}" to be of length shorter than or equal to ${length}`, UKNOWN_VALIDATOR: (name: string) => `encountered unknown string validator "${name}"`, }, number: { NUMBER_TYPE_VALIDATION_FAILED: (actualParam: string) => `expected "${actualParam}" to be of type number`, MIN_VALIDATION_FAILED: (actualParam: string, min: string) => `expected "${actualParam}" to be larger than or equal to ${min}`, MAX_VALIDATION_FAILED: (actualParam: string, max: string) => `expected "${actualParam}" to be lesser then or equal to ${max}`, MULTIPLE_OF_VALIDATION_FAILED: (actualParam: string, multipleOf: string) => `expected "${actualParam}" to be a multiple of ${multipleOf}`, UKNOWN_VALIDATOR: (name: string) => `encountered unknown number validator "${name}"`, }, enum: { ENUM_TYPE_VALIDATION_FAILED: (actualParam: string, options: string[]) => `expected "${actualParam}" to be one of the options: "${options.join(', ')}"`, NO_OPTIONS_DEFINED: () => `expected enum to be defined with one option or more`, }, }; export type SubValidator = (value: string, ...rest: string[]) => StateResult; export interface StateParamType { subValidators?: Record; validate( value: any, args: StateArguments, resolveParam: any, validateDefinition: boolean, validateValue: boolean ): StateResult; } export const systemValidators: Record = { string: { validate( value: any, validators: StateArguments, resolveParam: (s: string) => string, validateDefinition, validateValue ) { const res = value; const errors: string[] = []; if (validateValue && typeof value !== 'string') { errors.push(validationErrors.string.STRING_TYPE_VALIDATION_FAILED(value)); } if (validators.length > 0) { validators.forEach((validatorMeta) => { if (typeof validatorMeta === 'object') { if (this.subValidators && this.subValidators[validatorMeta.name]) { const subValidator = this.subValidators[validatorMeta.name]; const validationRes = subValidator( value, resolveParam(validatorMeta.args[0]) ); if (validateValue && validationRes.errors) { errors.push(...validationRes.errors); } } else if (validateDefinition) { errors.push( validationErrors.string.UKNOWN_VALIDATOR(validatorMeta.name) ); } } }); } return { res, errors: errors.length ? errors : null }; }, subValidators: { regex: (value: string, regex: string) => { const r = new RegExp(regex); const valid = r.test(value); return { res: value, errors: valid ? null : [validationErrors.string.REGEX_VALIDATION_FAILED(regex, value)], }; }, contains: (value: string, checkedValue: string) => { const valid = !!~value.indexOf(checkedValue); return { res: value, errors: valid ? null : [validationErrors.string.CONTAINS_VALIDATION_FAILED(checkedValue, value)], }; }, minLength: (value: string, length: string) => { const valid = value.length >= Number(length); return { res: value, errors: valid ? null : [validationErrors.string.MIN_LENGTH_VALIDATION_FAILED(length, value)], }; }, maxLength: (value: string, length: string) => { const valid = value.length <= Number(length); return { res: value, errors: valid ? null : [validationErrors.string.MAX_LENGTH_VALIDATION_FAILED(length, value)], }; }, }, }, number: { validate( value: any, validators: StateArguments, resolveParam: (s: string) => string, validateDefinition, validateValue ) { const res = value; const errors: string[] = []; if (isNaN(value)) { if (validateValue) { errors.push(validationErrors.number.NUMBER_TYPE_VALIDATION_FAILED(value)); } } else if (validators.length > 0) { validators.forEach((validatorMeta) => { if (typeof validatorMeta === 'object') { if (this.subValidators && this.subValidators[validatorMeta.name]) { const subValidator = this.subValidators[validatorMeta.name]; const validationRes = subValidator( value, resolveParam(validatorMeta.args[0]) ); if (validateValue && validationRes.errors) { errors.push(...validationRes.errors); } } else if (validateDefinition) { errors.push( validationErrors.number.UKNOWN_VALIDATOR(validatorMeta.name) ); } } }); } return { res, errors: errors.length ? errors : null }; }, subValidators: { min: (value: string, minValue: string) => { const valid = Number(value) >= Number(minValue); return { res: value, errors: valid ? null : [validationErrors.number.MIN_VALIDATION_FAILED(value, minValue)], }; }, max: (value: string, maxValue: string) => { const valid = Number(value) <= Number(maxValue); return { res: value, errors: valid ? null : [validationErrors.number.MAX_VALIDATION_FAILED(value, maxValue)], }; }, multipleOf: (value: string, multipleOf: string) => { const valid = Number(value) % Number(multipleOf) === 0; return { res: value, errors: valid ? null : [ validationErrors.number.MULTIPLE_OF_VALIDATION_FAILED( value, multipleOf ), ], }; }, }, }, enum: { validate( value: any, options: StateArguments, resolveParam: (s: string) => string, validateDefinition, validateValue ) { const res = value; const errors: string[] = []; const stringOptions: string[] = []; if (options.length) { const isOneOf = options.some((option) => { if (typeof option === 'string') { stringOptions.push(option); return resolveParam(option) === value; } return true; }); if (validateValue && !isOneOf) { errors.push( validationErrors.enum.ENUM_TYPE_VALIDATION_FAILED(value, stringOptions) ); } } else if (validateDefinition) { errors.push(validationErrors.enum.NO_OPTIONS_DEFINED()); } return { res, errors: errors.length ? errors : null }; }, }, }; export function validateRuleStateDefinition( selector: string, selectorNode: postcss.Rule | postcss.AtRule, meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics ) { const selectorAst = parseSelectorWithCache(selector); if (selectorAst.length && selectorAst.length === 1) { const singleSelectorAst = selectorAst[0]; const selectorChunk = singleSelectorAst.nodes; if (selectorChunk.length === 1 && selectorChunk[0].type === 'class') { const className = selectorChunk[0].value; const classMeta = CSSClass.get(meta, className); const states = classMeta?.[`-st-states`]; if (states && classMeta._kind === 'class') { for (const stateName in states) { // TODO: Sort out types const state = states[stateName]; if (state && typeof state === 'object') { const stateParam = isTemplateState(state) ? state.params[0] : state; const { errors } = validateStateArgument( stateParam, meta, stateParam.defaultValue || '', resolver, diagnostics, selectorNode, true, !!stateParam.defaultValue ); if (errors && selectorNode.nodes) { for (const node of selectorNode.nodes) { if (node.type === 'decl' && node.prop === `-st-states`) { diagnostics.report( stateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( stateName, stateParam.defaultValue || '', errors ), { node: node, word: node.value, } ); break; } } } } } } else { // TODO: error state on non-class } } } } export function validateStateArgument( stateAst: StateParsedValue, meta: StylableMeta, value: string, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode?: postcss.Node, validateDefinition?: boolean, validateValue = true ) { const resolvedValidations: StateResult = { res: resolveParam( meta, resolver, diagnostics, selectorNode, value || stateAst.defaultValue ), errors: null, }; const { type: paramType } = stateAst; const validator = systemValidators[paramType]; try { if (resolvedValidations.res || validateDefinition) { const { errors } = validator.validate( resolvedValidations.res, stateAst.arguments, resolveParam.bind(null, meta, resolver, diagnostics, selectorNode), !!validateDefinition, validateValue ); resolvedValidations.errors = errors; } } catch (error) { // TODO: warn about validation throwing exception } return resolvedValidations; } // transform export function transformPseudoClassToCustomState( stateDef: MappedStates[string], meta: StylableMeta, name: string, stateNode: PseudoClass, namespace: string, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode?: postcss.Node ) { if (stateDef === null || typeof stateDef === 'string') { if (stateNode.nodes && selectorNode) { diagnostics.report( stateDiagnostics.NO_PARAM_REQUIRED(name, stringifySelector(stateNode.nodes)), { node: selectorNode, word: stringifySelector(stateNode), } ); } if (stateDef === null) { // boolean convertToClass(stateNode).value = createBooleanStateClassName(name, namespace); } else { // static template selector // simply concat global mapped selector - ToDo: maybe change to 'selector' convertToInvalid(stateNode).value = stateDef; } delete stateNode.nodes; } else if (typeof stateDef === 'object') { if (isTemplateState(stateDef)) { convertTemplateState( meta, resolver, diagnostics, selectorNode, stateNode, stateDef, name ); } else { resolveStateValue( meta, resolver, diagnostics, selectorNode, stateNode, stateDef, name, namespace ); } } } export function isTemplateState(state: MappedStates[string]): state is TemplateStateParsedValue { return !!state && typeof state === 'object' && state.type === 'template'; } export function createBooleanStateClassName(stateName: string, namespace: string) { const escapedNamespace = cssesc(namespace, { isIdentifier: true }); return `${escapedNamespace}${booleanStateDelimiter}${stateName}`; } export function createStateWithParamClassName(stateName: string, namespace: string, param: string) { const escapedNamespace = cssesc(namespace, { isIdentifier: true }); return `${escapedNamespace}${stateWithParamDelimiter}${stateName}${resolveStateParam( param, true )}`; } export function resolveStateParam(param: string, escape = false) { const result = `${stateMiddleDelimiter}${param.length}${stateMiddleDelimiter}${param.replace( /\s/gm, '_' )}`; // adding/removing initial `s` to indicate that it's not the first param of the identifier return escape ? cssesc(`s` + result, { isIdentifier: true }).slice(1) : result; } function convertTemplateState( meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode: postcss.Node | undefined, stateNode: PseudoClass, stateParamDef: TemplateStateParsedValue, name: string ) { const paramStateDef = stateParamDef.params[0]; const resolvedParam = getParamInput( meta, resolver, diagnostics, selectorNode, stateNode, paramStateDef, name ); validateParam(meta, resolver, diagnostics, selectorNode, paramStateDef, resolvedParam, name); const strippedParam = stripQuotation(resolvedParam); transformMappedStateWithParam({ stateName: name, template: stateParamDef.template, param: strippedParam, node: stateNode, selectorNode: selectorNode, diagnostics, }); } function getParamInput( meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode: postcss.Node | undefined, stateNode: PseudoClass, stateParamDef: StateParsedValue, name: string ) { const inputValue = stateNode.nodes && stateNode.nodes.length ? stringifySelector(stateNode.nodes) : ``; const resolvedParam = resolveParam( meta, resolver, diagnostics, selectorNode, inputValue ? inputValue : stateParamDef.defaultValue ); if (selectorNode && !inputValue && !stateParamDef.defaultValue) { diagnostics.report(stateDiagnostics.NO_STATE_ARGUMENT_GIVEN(name, stateParamDef.type), { node: selectorNode, word: name, }); } return resolvedParam; } function validateParam( meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode: postcss.Node | undefined, stateParamDef: StateParsedValue, resolvedParam: string, name: string ) { const validator = systemValidators[stateParamDef.type]; let stateParamOutput: StateResult | undefined; try { stateParamOutput = validator.validate( resolvedParam, stateParamDef.arguments, resolveParam.bind(null, meta, resolver, diagnostics, selectorNode), false, true ); } catch (e) { // TODO: warn about validation throwing exception } if (stateParamOutput !== undefined) { if (stateParamOutput.res !== resolvedParam) { resolvedParam = stateParamOutput.res; } if (selectorNode && stateParamOutput.errors) { diagnostics.report( stateDiagnostics.FAILED_STATE_VALIDATION( name, resolvedParam, stateParamOutput.errors ), { node: selectorNode, word: resolvedParam, } ); } } } function resolveStateValue( meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics, selectorNode: postcss.Node | undefined, stateNode: PseudoClass, stateParamDef: StateParsedValue, name: string, namespace: string ) { const resolvedParam = getParamInput( meta, resolver, diagnostics, selectorNode, stateNode, stateParamDef, name ); validateParam(meta, resolver, diagnostics, selectorNode, stateParamDef, resolvedParam, name); const strippedParam = stripQuotation(resolvedParam); convertToClass(stateNode).value = createStateWithParamClassName(name, namespace, strippedParam); delete stateNode.nodes; } function transformMappedStateWithParam({ stateName, template, param, node, selectorNode, diagnostics, }: { stateName: string; template: string; param: string; node: PseudoClass; selectorNode?: postcss.Node; diagnostics: Diagnostics; }) { const selectorStr = template.replace(/\$0/g, param); const selectorAst = parseSelectorWithCache(selectorStr, { clone: true }); if ( !validateTemplateSelector({ stateName, selectorStr, selectorAst, cssNode: selectorNode, diagnostics, }) ) { return; } convertToSelector(node).nodes = selectorAst[0].nodes; } function validateTemplateSelector({ stateName, selectorStr, selectorAst, cssNode, diagnostics, }: { stateName: string; selectorStr: string; selectorAst: SelectorList; cssNode?: postcss.Node; diagnostics: Diagnostics; }): boolean { if (selectorAst.length > 1) { if (cssNode) { diagnostics.report( stateDiagnostics.UNSUPPORTED_MULTI_SELECTOR(stateName, selectorStr), { node: cssNode, } ); } return false; } else { const firstSelector = selectorAst[0].nodes.find(({ type }) => type !== 'comment'); if (firstSelector?.type === 'type' || firstSelector?.type === 'universal') { if (cssNode) { diagnostics.report( stateDiagnostics.UNSUPPORTED_INITIAL_SELECTOR(stateName, selectorStr), { node: cssNode, } ); } return false; } let unexpectedSelector: undefined | SelectorNode = undefined; for (const node of selectorAst[0].nodes) { if (node.type === 'combinator' || node.type === 'invalid') { unexpectedSelector = node; break; } } if (unexpectedSelector) { if (cssNode) { switch (unexpectedSelector.type) { case 'combinator': diagnostics.report( stateDiagnostics.UNSUPPORTED_COMPLEX_SELECTOR(stateName, selectorStr), { node: cssNode, } ); break; case 'invalid': diagnostics.report( stateDiagnostics.INVALID_SELECTOR(stateName, selectorStr), { node: cssNode, } ); break; } } return false; } } return true; } function resolveParam( meta: StylableMeta, resolver: StylableResolver, diagnostics: Diagnostics, node?: postcss.Node, nodeContent?: string ) { const defaultStringValue = ''; const param = nodeContent || defaultStringValue; return evalDeclarationValue(resolver, param, meta, node, undefined, undefined, diagnostics); }