UNPKG

@terrazzo/parser

Version:

Parser/validator for the Design Tokens Community Group (DTCG) standard.

302 lines 13.2 kB
import { isAlias, parseAlias, } from '@terrazzo/token-tools'; import { getObjMembers } from './json.js'; /** * Resolve aliases and update the token nodes. * * Data structures are in an awkward in-between phase, where they have * placeholders for data but we still need to resolve everything. As such, * TypeScript will raise errors expecting the final shape. * * This is also a bit tricky because different token types alias slightly * differently. For example, color tokens and other “primitive” tokens behave * as-expected. But composite tokens like Typography, Gradient, Border, etc. can * either fully- or partially-alias their values. Then we add modes to the mix, * and we have to do the work all over again for each mode declared. * * All that to say, there are a generous amount of TypeScript overrides here rather * than try to codify indeterminate shapes. */ export default function applyAliases(token, options) { // prepopulate default mode (if not set) token.mode['.'] ??= {}; token.mode['.'].$value = token.$value; token.mode['.'].originalValue ??= token.originalValue.$value; token.mode['.'].source ??= token.source; // resolve root if (typeof token.$value === 'string' && isAlias(token.$value)) { const { aliasChain, resolvedToken } = resolveAlias(token.$value, { ...options, token }); token.aliasOf = resolvedToken.id; token.aliasChain = aliasChain; token.$value = resolvedToken.$value; } // resolve modes for (const mode of Object.keys(token.mode)) { const modeValue = token.mode[mode].$value; // if the entire mode value is a simple alias, resolve & continue if (typeof modeValue === 'string' && isAlias(modeValue)) { const expectedType = [token.$type]; const { aliasChain, resolvedToken } = resolveAlias(modeValue, { ...options, token, expectedType, node: token.mode[mode].source?.node || options.node, }); token.mode[mode].aliasOf = resolvedToken.id; token.mode[mode].aliasChain = aliasChain; token.mode[mode].$value = resolvedToken.$value; continue; } // object types: expand default $value into current mode if (typeof token.$value === 'object' && typeof token.mode[mode].$value === 'object' && !Array.isArray(token.$value)) { for (const [k, v] of Object.entries(token.$value)) { if (!(k in token.mode[mode].$value)) { token.mode[mode].$value[k] = v; } } } // if the mode is an object or array that’s partially aliased, do work per-token type const node = getObjMembers(options.node).$value || options.node; switch (token.$type) { case 'border': { applyBorderPartialAlias(token, mode, { ...options, node }); break; } case 'gradient': { applyGradientPartialAlias(token, mode, { ...options, node }); break; } case 'shadow': { applyShadowPartialAlias(token, mode, { ...options, node }); break; } case 'strokeStyle': { applyStrokeStylePartialAlias(token, mode, { ...options, node }); break; } case 'transition': { applyTransitionPartialAlias(token, mode, { ...options, node }); break; } case 'typography': { applyTypographyPartialAlias(token, mode, { ...options, node }); break; } } } } const LIST_FORMAT = new Intl.ListFormat('en-us', { type: 'disjunction' }); /** * Resolve alias. Also add info on root node if it’s the root token (has .id) */ function resolveAlias(alias, options) { const baseMessage = { group: 'parser', label: 'alias', node: options?.node, filename: options.filename, src: options.src, }; const { logger, token, tokensSet } = options; const shallowAliasID = parseAlias(alias); const { token: resolvedToken, chain } = _resolveAliasInner(shallowAliasID, options); // Apply missing $types while resolving if (!tokensSet[token.id].$type) { tokensSet[token.id].$type = resolvedToken.$type; } // throw error if expectedType differed const expectedType = [...(options.expectedType ?? [])]; if (token.$type && !expectedType?.length) { expectedType.push(token.$type); } if (expectedType?.length && !expectedType.includes(resolvedToken.$type)) { logger.error({ ...baseMessage, message: `Invalid alias: expected $type: ${LIST_FORMAT.format(expectedType)}, received $type: ${resolvedToken.$type}.`, node: (options.node?.type === 'Object' && getObjMembers(options.node).$value) || baseMessage.node, }); } // Apply reverse aliases as we’re traversing the graph if (chain?.length && resolvedToken) { let needsSort = false; for (const id of chain) { if (id !== resolvedToken.id && !resolvedToken.aliasedBy?.includes(id)) { resolvedToken.aliasedBy ??= []; resolvedToken.aliasedBy.push(id); needsSort = true; } if (token && !resolvedToken.aliasedBy?.includes(token.id)) { resolvedToken.aliasedBy ??= []; resolvedToken.aliasedBy.push(token.id); needsSort = true; } } if (needsSort) { resolvedToken.aliasedBy.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true })); } } return { resolvedToken: resolvedToken, aliasChain: chain }; } function _resolveAliasInner(alias, { scanned = [], ...options }) { const { logger, filename, src, node, tokensSet } = options; const baseMessage = { group: 'parser', label: 'alias', filename, src, node }; const id = parseAlias(alias); if (!tokensSet[id]) { logger.error({ ...baseMessage, message: `Alias {${alias}} not found.` }); } if (scanned.includes(id)) { logger.error({ ...baseMessage, message: `Circular alias detected from ${alias}.` }); } const token = tokensSet[id]; scanned.push(id); // important: use originalValue to trace the full alias path correctly // finish resolution if (typeof token.originalValue.$value !== 'string' || !isAlias(token.originalValue.$value)) { return { token, chain: scanned }; } // continue resolving return _resolveAliasInner(token.originalValue.$value, { ...options, scanned }); } function applyBorderPartialAlias(token, mode, options) { for (const [k, v] of Object.entries(token.mode[mode].$value)) { if (typeof v === 'string' && isAlias(v)) { token.mode[mode].partialAliasOf ??= {}; const node = getObjMembers(options.node)[k] || options.node; const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType: { color: ['color'], width: ['dimension'], style: ['strokeStyle'] }[k], node, }); token.mode[mode].partialAliasOf[k] = parseAlias(v); if (mode === '.') { token.partialAliasOf ??= {}; token.partialAliasOf[k] = parseAlias(v); } token.mode[mode].$value[k] = resolvedToken.$value; } } } function applyGradientPartialAlias(token, mode, options) { for (let i = 0; i < token.mode[mode].$value.length; i++) { const step = token.mode[mode].$value[i]; for (const [k, v] of Object.entries(step)) { if (typeof v === 'string' && isAlias(v)) { token.mode[mode].partialAliasOf ??= []; token.mode[mode].partialAliasOf[i] ??= {}; const expectedType = { color: ['color'], position: ['number'] }[k]; let node = options.node?.elements?.[i]?.value || options.node; if (node.type === 'Object') { node = getObjMembers(node)[k] || node; } const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node }); token.mode[mode].partialAliasOf[i][k] = parseAlias(v); if (mode === '.') { token.partialAliasOf ??= []; token.partialAliasOf[i] ??= {}; token.partialAliasOf[i][k] = parseAlias(v); } step[k] = resolvedToken.$value; } } } } function applyShadowPartialAlias(token, mode, options) { // shadow-only fix: historically this token type may or may not allow an array // of values, and at this stage in parsing, they all might not have been // normalized yet. if (!Array.isArray(token.mode[mode].$value)) { token.mode[mode].$value = [token.mode[mode].$value]; } for (let i = 0; i < token.mode[mode].$value.length; i++) { const layer = token.mode[mode].$value[i]; for (const [k, v] of Object.entries(layer)) { if (typeof v === 'string' && isAlias(v)) { token.mode[mode].partialAliasOf ??= []; token.mode[mode].partialAliasOf[i] ??= {}; const expectedType = { offsetX: ['dimension'], offsetY: ['dimension'], blur: ['dimension'], spread: ['dimension'], color: ['color'], inset: ['boolean'], }[k]; let node = options.node?.elements?.[i] || options.node; if (node.type === 'Object') { node = getObjMembers(node)[k] || node; } const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node }); token.mode[mode].partialAliasOf[i][k] = parseAlias(v); if (mode === '.') { token.partialAliasOf ??= []; token.partialAliasOf[i] ??= {}; token.partialAliasOf[i][k] = parseAlias(v); } layer[k] = resolvedToken.$value; } } } } function applyStrokeStylePartialAlias(token, mode, options) { // only dashArray can be aliased if (typeof token.mode[mode].$value !== 'object' || !('dashArray' in token.mode[mode].$value)) { return; } for (let i = 0; i < token.mode[mode].$value.dashArray.length; i++) { const dash = token.mode[mode].$value.dashArray[i]; if (typeof dash === 'string' && isAlias(dash)) { let node = getObjMembers(options.node).dashArray || options.node; if (node.type === 'Array') { node = node?.elements?.[i]?.value || node; } const { resolvedToken } = resolveAlias(dash, { ...options, token, expectedType: ['dimension'], node, }); token.mode[mode].$value.dashArray[i] = resolvedToken.$value; } } } function applyTransitionPartialAlias(token, mode, options) { for (const [k, v] of Object.entries(token.mode[mode].$value)) { if (typeof v === 'string' && isAlias(v)) { token.mode[mode].partialAliasOf ??= {}; const expectedType = { duration: ['duration'], delay: ['duration'], timingFunction: ['cubicBezier'] }[k]; const node = getObjMembers(options.node)[k] || options.node; const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node }); token.mode[mode].partialAliasOf[k] = parseAlias(v); if (mode === '.') { token.partialAliasOf ??= {}; token.partialAliasOf[k] = parseAlias(v); } token.mode[mode].$value[k] = resolvedToken.$value; } } } function applyTypographyPartialAlias(token, mode, options) { for (const [k, v] of Object.entries(token.mode[mode].$value)) { if (typeof v === 'string' && isAlias(v)) { token.partialAliasOf ??= {}; token.mode[mode].partialAliasOf ??= {}; const expectedType = { fontFamily: ['fontFamily'], fontSize: ['dimension'], fontWeight: ['fontWeight'], letterSpacing: ['dimension'], lineHeight: ['dimension', 'number'], }[k] || ['string']; const node = getObjMembers(options.node)[k] || options.node; const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node }); token.mode[mode].partialAliasOf[k] = parseAlias(v); if (mode === '.') { token.partialAliasOf[k] = parseAlias(v); } token.mode[mode].$value[k] = resolvedToken.$value; } } } //# sourceMappingURL=alias.js.map