@stylable/core
Version:
CSS for Components
383 lines (368 loc) • 18 kB
text/typescript
import postcss from 'postcss';
import { resolveCustomValues, stTypes } from './custom-values';
import { Diagnostics } from './diagnostics';
import { isCssNativeFunction } from './native-reserved-lists';
import { StylableMeta } from './stylable-processor';
import { CSSResolve, JSResolve, StylableResolver } from './stylable-resolver';
import { replaceValueHook, StylableTransformer } from './stylable-transformer';
import { isCSSVarProp } from './stylable-utils';
import {
getFormatterArgs,
getStringValue,
strategies,
valueMapping,
} from './stylable-value-parsers';
import { ParsedValue } from './types';
import { stripQuotation } from './utils';
const postcssValueParser = require('postcss-value-parser');
export type ValueFormatter = (name: string) => string;
export type ResolvedFormatter = Record<string, JSResolve | CSSResolve | ValueFormatter | null>;
export const functionWarnings = {
FAIL_TO_EXECUTE_FORMATTER: (resolvedValue: string, message: string) =>
`failed to execute formatter "${resolvedValue}" with error: "${message}"`,
CYCLIC_VALUE: (cyclicChain: string[]) =>
`Cyclic value definition detected: "${cyclicChain
.map((s, i) => (i === cyclicChain.length - 1 ? '↻ ' : i === 0 ? '→ ' : '↪ ') + s)
.join('\n')}"`,
CANNOT_USE_AS_VALUE: (type: string, varName: string) =>
`${type} "${varName}" cannot be used as a variable`,
CANNOT_USE_JS_AS_VALUE: (varName: string) =>
`JavaScript import "${varName}" cannot be used as a variable`,
CANNOT_FIND_IMPORTED_VAR: (varName: string) => `cannot use unknown imported "${varName}"`,
MULTI_ARGS_IN_VALUE: (args: string) =>
`value function accepts only a single argument: "value(${args})"`,
COULD_NOT_RESOLVE_VALUE: (args: string) =>
`cannot resolve value function using the arguments provided: "${args}"`,
UNKNOWN_FORMATTER: (name: string) =>
`cannot find native function or custom formatter called ${name}`,
UNKNOWN_VAR: (name: string) => `unknown var "${name}"`,
};
export function resolveArgumentsValue(
options: Record<string, string>,
transformer: StylableTransformer,
meta: StylableMeta,
diagnostics: Diagnostics,
node: postcss.Node,
variableOverride?: Record<string, string>,
path?: string[],
cssVarsMapping?: Record<string, string>
) {
const resolvedArgs = {} as Record<string, string>;
for (const k in options) {
resolvedArgs[k] = evalDeclarationValue(
transformer.resolver,
options[k],
meta,
node,
variableOverride,
transformer.replaceValueHook,
diagnostics,
path,
cssVarsMapping
);
}
return resolvedArgs;
}
export function processDeclarationValue(
resolver: StylableResolver,
value: string,
meta: StylableMeta,
node?: postcss.Node,
variableOverride?: Record<string, string> | null,
valueHook?: replaceValueHook,
diagnostics?: Diagnostics,
passedThrough: string[] = [],
cssVarsMapping?: Record<string, string>,
args: string[] = []
): { topLevelType: any; outputValue: string; typeError: Error } {
diagnostics = node ? diagnostics : undefined;
const customValues = resolveCustomValues(meta, resolver);
const parsedValue = postcssValueParser(value);
parsedValue.walk((parsedNode: ParsedValue) => {
const { type, value } = parsedNode;
switch (type) {
case 'function':
if (value === 'value') {
const parsedArgs = strategies.args(parsedNode).map((x) => x.value);
if (parsedArgs.length >= 1) {
const varName = parsedArgs[0];
const getArgs = parsedArgs
.slice(1)
.map((arg) =>
evalDeclarationValue(
resolver,
arg,
meta,
node,
variableOverride,
valueHook,
diagnostics,
passedThrough.concat(createUniqID(meta.source, varName)),
cssVarsMapping
)
);
if (variableOverride && variableOverride[varName]) {
return (parsedNode.resolvedValue = variableOverride[varName]);
}
const refUniqID = createUniqID(meta.source, varName);
if (passedThrough.includes(refUniqID)) {
// TODO: move diagnostic to original value usage instead of the end of the cyclic chain
return handleCyclicValues(
passedThrough,
refUniqID,
diagnostics,
node,
value,
parsedNode
);
}
const varSymbol = meta.mappedSymbols[varName];
if (varSymbol && varSymbol._kind === 'var') {
const resolved = processDeclarationValue(
resolver,
stripQuotation(varSymbol.text),
meta,
varSymbol.node,
variableOverride,
valueHook,
diagnostics,
passedThrough.concat(createUniqID(meta.source, varName)),
cssVarsMapping,
getArgs
);
const { outputValue, topLevelType, typeError } = resolved;
if (diagnostics && node) {
const argsAsString = parsedArgs.join(', ');
if (typeError) {
diagnostics.warn(
node,
functionWarnings.COULD_NOT_RESOLVE_VALUE(argsAsString)
);
} else if (!topLevelType && parsedArgs.length > 1) {
diagnostics.warn(
node,
functionWarnings.MULTI_ARGS_IN_VALUE(argsAsString)
);
}
}
parsedNode.resolvedValue = valueHook
? valueHook(outputValue, varName, true, passedThrough)
: outputValue;
} else if (varSymbol && varSymbol._kind === 'import') {
const resolvedVar = resolver.deepResolve(varSymbol);
if (resolvedVar && resolvedVar.symbol) {
const resolvedVarSymbol = resolvedVar.symbol;
if (resolvedVar._kind === 'css') {
if (resolvedVarSymbol._kind === 'var') {
const resolvedValue = evalDeclarationValue(
resolver,
stripQuotation(resolvedVarSymbol.text),
resolvedVar.meta,
resolvedVarSymbol.node,
variableOverride,
valueHook,
diagnostics,
passedThrough.concat(
createUniqID(meta.source, varName)
),
cssVarsMapping,
getArgs
);
parsedNode.resolvedValue = valueHook
? valueHook(
resolvedValue,
varName,
false,
passedThrough
)
: resolvedValue;
} else {
const errorKind =
resolvedVarSymbol._kind === 'class' &&
resolvedVarSymbol[valueMapping.root]
? 'stylesheet'
: resolvedVarSymbol._kind;
if (diagnostics && node) {
diagnostics.warn(
node,
functionWarnings.CANNOT_USE_AS_VALUE(
errorKind,
varName
),
{ word: varName }
);
}
}
} else if (resolvedVar._kind === 'js' && diagnostics && node) {
// ToDo: provide actual exported id (default/named as x)
diagnostics.warn(
node,
functionWarnings.CANNOT_USE_JS_AS_VALUE(varName),
{
word: varName,
}
);
}
} else {
const namedDecl = varSymbol.import.rule.nodes!.find((node) => {
return node.type === 'decl' && node.prop === valueMapping.named;
});
if (namedDecl && diagnostics && node) {
// ToDo: provide actual exported id (default/named as x)
diagnostics.error(
node,
functionWarnings.CANNOT_FIND_IMPORTED_VAR(varName),
{ word: varName }
);
}
}
} else if (diagnostics && node) {
diagnostics.warn(node, functionWarnings.UNKNOWN_VAR(varName), {
word: varName,
});
}
}
} else if (value === '') {
parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
} else {
if (customValues[value]) {
// no op resolved at the bottom
} else if (value === 'url') {
// postcss-value-parser treats url differently:
// https://github.com/TrySound/postcss-value-parser/issues/34
} else if (value === 'format') {
// perserve native format function quotation
parsedNode.resolvedValue = stringifyFunction(value, parsedNode, true);
} else {
const formatterRef = meta.mappedSymbols[value];
const formatter = resolver.deepResolve(formatterRef);
const formatterArgs = getFormatterArgs(parsedNode);
if (formatter && formatter._kind === 'js') {
try {
parsedNode.resolvedValue = formatter.symbol.apply(
null,
formatterArgs
);
if (valueHook && typeof parsedNode.resolvedValue === 'string') {
parsedNode.resolvedValue = valueHook(
parsedNode.resolvedValue,
{ name: parsedNode.value, args: formatterArgs },
true,
passedThrough
);
}
} catch (error) {
parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
if (diagnostics && node) {
diagnostics.warn(
node,
functionWarnings.FAIL_TO_EXECUTE_FORMATTER(
parsedNode.resolvedValue,
error.message
),
{ word: (node as postcss.Declaration).value }
);
}
}
} else if (value === 'var') {
const varWithPrefix = parsedNode.nodes[0].value;
if (isCSSVarProp(varWithPrefix)) {
if (cssVarsMapping && cssVarsMapping[varWithPrefix]) {
parsedNode.nodes[0].value = cssVarsMapping[varWithPrefix];
}
}
// handle default values
if (parsedNode.nodes.length > 2) {
parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
}
} else if (isCssNativeFunction(value)) {
parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
} else if (diagnostics && node) {
parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
diagnostics.warn(node, functionWarnings.UNKNOWN_FORMATTER(value), {
word: value,
});
}
}
}
break;
default: {
return postcssValueParser.stringify(parsedNode);
}
}
}, true);
let outputValue = '';
let topLevelType = null;
let typeError = null;
for (const n of parsedValue.nodes) {
if (n.type === 'function') {
const matchingType = customValues[n.value as keyof typeof stTypes];
if (matchingType) {
topLevelType = matchingType.evalVarAst(n, customValues);
try {
outputValue += matchingType.getValue(args, topLevelType, n, customValues);
} catch (e) {
typeError = e;
// catch broken variable resolutions
}
} else {
outputValue += getStringValue([n]);
}
} else {
outputValue += getStringValue([n]);
}
}
return { outputValue, topLevelType, typeError };
// }
// TODO: handle calc (parse internals but maintain expression)
// TODO: check this thing. native function that accent our function dose not work
// e.g: calc(getVarName())
}
export function evalDeclarationValue(
resolver: StylableResolver,
value: string,
meta: StylableMeta,
node?: postcss.Node,
variableOverride?: Record<string, string> | null,
valueHook?: replaceValueHook,
diagnostics?: Diagnostics,
passedThrough: string[] = [],
cssVarsMapping?: Record<string, string>,
args: string[] = []
): string {
return processDeclarationValue(
resolver,
value,
meta,
node,
variableOverride,
valueHook,
diagnostics,
passedThrough,
cssVarsMapping,
args
).outputValue;
}
function handleCyclicValues(
passedThrough: string[],
refUniqID: string,
diagnostics: Diagnostics | undefined,
node: postcss.Node | undefined,
value: string,
parsedNode: ParsedValue
) {
const cyclicChain = passedThrough.map((variable) => variable || '');
cyclicChain.push(refUniqID);
if (diagnostics && node) {
diagnostics.warn(node, functionWarnings.CYCLIC_VALUE(cyclicChain), {
word: refUniqID,
});
}
return stringifyFunction(value, parsedNode);
}
function stringifyFunction(name: string, parsedNode: ParsedValue, perserveQuotes = false) {
return `${name}(${getFormatterArgs(parsedNode, false, undefined, perserveQuotes).join(', ')})`;
}
function createUniqID(source: string, varName: string) {
return `${source}: ${varName}`;
}