@terrazzo/parser
Version:
Parser/validator for the Design Tokens Community Group (DTCG) standard.
210 lines (205 loc) • 6.77 kB
text/typescript
import {
type ColorValueNormalized,
type CubicBezierValue,
type DimensionValue,
type FontFamilyValue,
type GradientStopNormalized,
type GradientValueNormalized,
type ShadowValueNormalized,
type Token,
type TransitionValue,
type TypographyValueNormalized,
isAlias,
parseColor,
} from '@terrazzo/token-tools';
export const FONT_WEIGHT_MAP = {
thin: 100,
hairline: 100,
'extra-light': 200,
'ultra-light': 200,
light: 300,
normal: 400,
regular: 400,
book: 400,
medium: 500,
'semi-bold': 600,
'demi-bold': 600,
bold: 700,
'extra-bold': 800,
'ultra-bold': 800,
black: 900,
heavy: 900,
'extra-black': 950,
'ultra-black': 950,
};
// Note: because we’re handling a lot of input values, the type inference gets lost.
// This file is expected to have a lot of `@ts-ignore` comments.
const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
/** Fill in defaults, and return predictable shapes for tokens */
export default function normalizeValue<T extends Token>(token: T): T['$value'] {
if (typeof token.$value === 'string' && isAlias(token.$value)) {
return token.$value;
}
switch (token.$type) {
case 'boolean': {
return !!token.$value;
}
case 'border': {
if (typeof token.$value === 'string') {
return token.$value;
}
return {
color: normalizeValue({ $type: 'color', $value: token.$value.color ?? '#000000' }),
style: normalizeValue({ $type: 'strokeStyle', $value: token.$value.style ?? 'solid' }),
width: normalizeValue({ $type: 'dimension', $value: token.$value.width }),
};
}
case 'color': {
if (typeof token.$value === 'string') {
return parseColor(token.$value);
}
const newValue: ColorValueNormalized = {
colorSpace: token.$value.colorSpace,
components: token.$value.components ?? token.$value.channels,
alpha: token.$value.alpha ?? 1,
};
if ('hex' in token.$value) {
newValue.hex = token.$value.hex;
}
return newValue;
}
case 'cubicBezier': {
if (typeof token.$value === 'string') {
return token.$value;
}
return token.$value.map((value) =>
typeof value === 'number' ? normalizeValue({ $type: 'number', $value: value }) : value,
) as CubicBezierValue;
}
case 'dimension': {
if ((token as any).$value === 0) {
return { value: 0, unit: 'px' };
}
// Backwards compat: handle string
if (typeof token.$value === 'string') {
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
return { value: Number.parseFloat(match?.[1] || token.$value), unit: match?.[2] || 'px' };
}
return token.$value;
}
case 'duration': {
if ((token as any).$value === 0) {
return { value: 0, unit: 'ms' };
}
// Backwards compat: handle string
if (typeof token.$value === 'string') {
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
return { value: Number.parseFloat(match?.[1] || token.$value), unit: match?.[2] || 'ms' };
}
return token.$value;
}
case 'fontFamily': {
return Array.isArray(token.$value) ? token.$value : [token.$value];
}
case 'fontWeight': {
if (typeof token.$value === 'string' && FONT_WEIGHT_MAP[token.$value as keyof typeof FONT_WEIGHT_MAP]) {
return FONT_WEIGHT_MAP[token.$value as keyof typeof FONT_WEIGHT_MAP];
}
return Math.min(
999,
Math.max(1, typeof token.$value === 'string' ? Number.parseInt(token.$value) : token.$value),
);
}
case 'gradient': {
if (typeof token.$value === 'string') {
return token.$value;
}
const output: GradientValueNormalized = [];
for (let i = 0; i < token.$value.length; i++) {
const stop = structuredClone(token.$value[i] as GradientStopNormalized);
stop.color = normalizeValue({ $type: 'color', $value: stop.color! });
if (stop.position === undefined) {
stop.position = i / (token.$value.length - 1);
}
output.push(stop);
}
return output;
}
case 'number': {
return typeof token.$value === 'number' ? token.$value : Number.parseFloat(token.$value);
}
case 'shadow': {
if (typeof token.$value === 'string') {
return token.$value;
}
return (Array.isArray(token.$value) ? token.$value : [token.$value]).map(
(layer) =>
({
color: normalizeValue({ $type: 'color', $value: layer.color }),
// @ts-ignore
offsetX: normalizeValue({ $type: 'dimension', $value: layer.offsetX ?? 0 }),
// @ts-ignore
offsetY: normalizeValue({ $type: 'dimension', $value: layer.offsetY ?? 0 }),
// @ts-ignore
blur: normalizeValue({ $type: 'dimension', $value: layer.blur ?? 0 }),
// @ts-ignore
spread: normalizeValue({ $type: 'dimension', $value: layer.spread ?? 0 }),
inset: layer.inset === true,
}) as ShadowValueNormalized,
);
}
case 'strokeStyle': {
return token.$value;
}
case 'string': {
return String(token.$value);
}
case 'transition': {
if (typeof token.$value === 'string') {
return token.$value;
}
return {
// @ts-ignore
duration: normalizeValue({ $type: 'duration', $value: token.$value.duration ?? 0 }),
// @ts-ignore
delay: normalizeValue({ $type: 'duration', $value: token.$value.delay ?? 0 }),
// @ts-ignore
timingFunction: normalizeValue({ $type: 'cubicBezier', $value: token.$value.timingFunction }),
} as TransitionValue;
}
case 'typography': {
if (typeof token.$value === 'string') {
return token.$value;
}
const output: TypographyValueNormalized = {};
for (const [k, $value] of Object.entries(token.$value)) {
switch (k) {
case 'fontFamily': {
output[k] = normalizeValue({ $type: 'fontFamily', $value: $value as FontFamilyValue });
break;
}
case 'fontSize':
case 'letterSpacing': {
output[k] = normalizeValue({ $type: 'dimension', $value: $value as DimensionValue });
break;
}
case 'lineHeight': {
output[k] = normalizeValue({
$type: typeof token.$value === 'number' ? 'number' : 'dimension',
$value: $value as any,
});
break;
}
default: {
output[k] = $value;
break;
}
}
}
return output;
}
default: {
return token.$value;
}
}
}