|
|
|
|
import {
|
|
|
|
|
ASPECT_RATIO_KEYS,
|
|
|
|
|
BORDER_STYLE_KEYS,
|
|
|
|
|
BORDER_WIDTH_KEYS,
|
|
|
|
|
COLOR_ROLES,
|
|
|
|
|
COLOR_SCALE_STEPS,
|
|
|
|
|
CONTAINER_WIDTH_KEYS,
|
|
|
|
|
CONTENT_WIDTH_KEYS,
|
|
|
|
|
OPACITY_KEYS,
|
|
|
|
|
SHADOW_KEYS,
|
|
|
|
|
SIZE_PRIMITIVE_KEYS,
|
|
|
|
|
TEXT_SIZES,
|
|
|
|
|
Z_INDEX_KEYS,
|
|
|
|
|
type BorderPrimitiveSet,
|
|
|
|
|
type ColorRole,
|
|
|
|
|
type ColorRoleDefinition,
|
|
|
|
|
type ColorScale,
|
|
|
|
|
type EidosCssContract,
|
|
|
|
|
type EidosValidationIssue,
|
|
|
|
|
type EidosValidationReport,
|
|
|
|
|
type EidosConfig,
|
|
|
|
|
type EidosConfigSnapshot,
|
|
|
|
|
type SizePrimitive,
|
|
|
|
|
type TextMetric,
|
|
|
|
|
type ThemeDefinition,
|
|
|
|
|
type TypographyPrimitiveSet
|
|
|
|
|
} from './config-types'
|
|
|
|
|
import { createEidosCssContract } from './contract'
|
|
|
|
|
import {
|
|
|
|
|
createEidosConfigDocument as createEidosConfigDocumentEnvelope,
|
|
|
|
|
parseEidosConfigDocument as parseEidosConfigDocumentEnvelope,
|
|
|
|
|
readEidosConfigDocument as readEidosConfigDocumentEnvelope,
|
|
|
|
|
serializeEidosConfigDocument as serializeEidosConfigDocumentEnvelope,
|
|
|
|
|
type EidosConfigDocument
|
|
|
|
|
} from './persistence'
|
|
|
|
|
import { EidosConfigValidationError } from '../errors'
|
|
|
|
|
|
|
|
|
|
const COLOR_ROLE_SET = new Set<string>(COLOR_ROLES)
|
|
|
|
|
const ASPECT_RATIO_KEY_SET = new Set<string>(ASPECT_RATIO_KEYS)
|
|
|
|
|
const BORDER_WIDTH_KEY_SET = new Set<string>(BORDER_WIDTH_KEYS)
|
|
|
|
|
const BORDER_STYLE_KEY_SET = new Set<string>(BORDER_STYLE_KEYS)
|
|
|
|
|
const CONTAINER_WIDTH_KEY_SET = new Set<string>(CONTAINER_WIDTH_KEYS)
|
|
|
|
|
const CONTENT_WIDTH_KEY_SET = new Set<string>(CONTENT_WIDTH_KEYS)
|
|
|
|
|
const OPACITY_KEY_SET = new Set<string>(OPACITY_KEYS)
|
|
|
|
|
const SHADOW_KEY_SET = new Set<string>(SHADOW_KEYS)
|
|
|
|
|
const SIZE_PRIMITIVE_KEY_SET = new Set<string>(SIZE_PRIMITIVE_KEYS)
|
|
|
|
|
const TEXT_SIZE_SET = new Set<string>(TEXT_SIZES)
|
|
|
|
|
const Z_INDEX_KEY_SET = new Set<string>(Z_INDEX_KEYS)
|
|
|
|
|
const RESPONSIVE_KEY_SET = new Set(['base', 'sm', 'md', 'lg', 'xl', 'xxl'])
|
|
|
|
|
|
|
|
|
|
export function snapshotEidosConfig(options: EidosConfig): EidosConfigSnapshot {
|
|
|
|
|
return clonePlain(options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function readEidosConfigFromDocument(document: unknown): EidosConfig {
|
|
|
|
|
return snapshotEidosConfig(readEidosConfigDocumentEnvelope(document).options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseEidosConfigFromJson(json: string): EidosConfig {
|
|
|
|
|
return snapshotEidosConfig(parseEidosConfigDocumentEnvelope(json).options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createEidosConfigDocumentFromConfig(options: EidosConfig): EidosConfigDocument {
|
|
|
|
|
assertValidEidosConfig(options)
|
|
|
|
|
|
|
|
|
|
return createEidosConfigDocumentEnvelope(options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function serializeEidosConfig(options: EidosConfig): string {
|
|
|
|
|
return serializeEidosConfigDocumentEnvelope(createEidosConfigDocumentFromConfig(options))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateEidosConfig(options: EidosConfig): EidosValidationReport {
|
|
|
|
|
const issues: EidosValidationIssue[] = []
|
|
|
|
|
|
|
|
|
|
validateColorScales(options, issues)
|
|
|
|
|
validateColorRoles(options, issues)
|
|
|
|
|
validateTypography(options, issues)
|
|
|
|
|
validateSizePrimitives(options, issues)
|
|
|
|
|
validateBorderPrimitives(options, issues)
|
|
|
|
|
validateLayoutPrimitives(options, issues)
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.opacity',
|
|
|
|
|
options.primitives.opacity,
|
|
|
|
|
OPACITY_KEYS,
|
|
|
|
|
OPACITY_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.zIndex',
|
|
|
|
|
options.primitives.zIndex,
|
|
|
|
|
Z_INDEX_KEYS,
|
|
|
|
|
Z_INDEX_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateThemeShadows(options, issues)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: issues.length === 0,
|
|
|
|
|
issues
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function assertValidEidosConfig(options: EidosConfig): void {
|
|
|
|
|
const report = validateEidosConfig(options)
|
|
|
|
|
if (report.ok) return
|
|
|
|
|
throw new EidosConfigValidationError(report.issues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listEidosColorScales(options: EidosConfig): readonly string[] {
|
|
|
|
|
return Object.keys(options.primitives.color?.scales ?? {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listEidosThemes(options: EidosConfig): readonly string[] {
|
|
|
|
|
return Object.keys(options.themes ?? {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getEidosTheme(options: EidosConfig, id: string): ThemeDefinition | undefined {
|
|
|
|
|
const theme = options.themes?.[id]
|
|
|
|
|
return theme ? clonePlain(theme) : undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listEidosColorRoles(): readonly ColorRole[] {
|
|
|
|
|
return [...COLOR_ROLES]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getEidosColorScale(options: EidosConfig, name: string): ColorScale | undefined {
|
|
|
|
|
const scale = options.primitives.color?.scales?.[name]
|
|
|
|
|
return scale ? clonePlain(scale) : undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getEidosColorRoleScale(
|
|
|
|
|
options: EidosConfig,
|
|
|
|
|
role: ColorRole,
|
|
|
|
|
themeId?: string
|
|
|
|
|
): ColorScale | undefined {
|
|
|
|
|
const theme = themeId ? options.themes?.[themeId] : undefined
|
|
|
|
|
const roleDef = theme?.color?.roles?.[role] ?? options.semantics.color.roles[role]
|
|
|
|
|
const scaleName = resolveRoleScaleName(roleDef)
|
|
|
|
|
if (!scaleName) return undefined
|
|
|
|
|
|
|
|
|
|
const themeScale = theme?.color?.scales?.[scaleName]
|
|
|
|
|
if (themeScale) return clonePlain(themeScale)
|
|
|
|
|
|
|
|
|
|
return getEidosColorScale(options, scaleName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getEidosCssContract(options: EidosConfig): EidosCssContract {
|
|
|
|
|
assertValidEidosConfig(options)
|
|
|
|
|
|
|
|
|
|
return createEidosCssContract(options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateColorScales(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const scales = options.primitives.color?.scales ?? {}
|
|
|
|
|
|
|
|
|
|
for (const [name, scale] of Object.entries(scales)) {
|
|
|
|
|
validateColorScale(`primitives.color.scales.${name}`, name, scale, issues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [themeId, theme] of Object.entries(options.themes ?? {})) {
|
|
|
|
|
for (const [name, scale] of Object.entries(theme.color?.scales ?? {})) {
|
|
|
|
|
validateColorScale(`themes.${themeId}.color.scales.${name}`, name, scale, issues)
|
|
|
|
|
}
|
|
|
|
|
for (const [name, scale] of Object.entries(theme.color?.alphaScales ?? {})) {
|
|
|
|
|
validateColorScale(`themes.${themeId}.color.alphaScales.${name}`, name, scale, issues)
|
|
|
|
|
if (
|
|
|
|
|
!hasOwn(options.primitives.color?.scales ?? {}, name) &&
|
|
|
|
|
!hasOwn(theme.color?.scales ?? {}, name)
|
|
|
|
|
) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `themes.${themeId}.color.alphaScales.${name}`,
|
|
|
|
|
message: `alpha scale "${name}" must match a configured color scale`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateColorRoles(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const roles = options.semantics.color.roles
|
|
|
|
|
const globalScales = options.primitives.color?.scales ?? {}
|
|
|
|
|
const themes = options.themes ?? {}
|
|
|
|
|
|
|
|
|
|
for (const role of COLOR_ROLES) {
|
|
|
|
|
if (!hasOwn(roles, role)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `semantics.color.roles.${role}`,
|
|
|
|
|
message: 'every canonical color role must be mapped to a primitive scale'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [role, roleDef] of Object.entries(roles)) {
|
|
|
|
|
validateColorRole(`semantics.color.roles.${role}`, role, roleDef, globalScales, themes, issues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [themeId, theme] of Object.entries(themes)) {
|
|
|
|
|
for (const [role, roleDef] of Object.entries(theme.color?.roles ?? {})) {
|
|
|
|
|
validateColorRole(
|
|
|
|
|
`themes.${themeId}.color.roles.${role}`,
|
|
|
|
|
role,
|
|
|
|
|
roleDef,
|
|
|
|
|
globalScales,
|
|
|
|
|
{ [themeId]: theme },
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateColorScale(
|
|
|
|
|
path: string,
|
|
|
|
|
name: string,
|
|
|
|
|
scale: ColorScale,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
for (const step of COLOR_SCALE_STEPS) {
|
|
|
|
|
if (!hasOwn(scale, step) || scale[step] === '') {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.${step}`,
|
|
|
|
|
message: 'color scales must define every step from 1 to 12'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateColorRole(
|
|
|
|
|
path: string,
|
|
|
|
|
role: string,
|
|
|
|
|
roleDef: string | ColorRoleDefinition | undefined,
|
|
|
|
|
globalScales: Record<string, ColorScale>,
|
|
|
|
|
themes: Record<string, ThemeDefinition>,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (!COLOR_ROLE_SET.has(role)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: 'unknown color role'
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scaleName = resolveRoleScaleName(roleDef)
|
|
|
|
|
if (!scaleName) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: 'color role must point to a primitive scale'
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const themeEntries = Object.entries(themes)
|
|
|
|
|
if (themeEntries.length === 0) {
|
|
|
|
|
if (!hasOwn(globalScales, scaleName)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: `color role points to missing primitive scale "${scaleName}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [themeId, theme] of themeEntries) {
|
|
|
|
|
if (!hasOwn(globalScales, scaleName) && !hasOwn(theme.color?.scales ?? {}, scaleName)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `themes.${themeId}.color.scales.${scaleName}`,
|
|
|
|
|
message: `color role "${role}" points to a scale missing in this theme`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateTypography(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const typography = options.primitives.typography
|
|
|
|
|
if (!typography) return
|
|
|
|
|
|
|
|
|
|
for (const size of TEXT_SIZES) {
|
|
|
|
|
const metric = typography.sizes[size]
|
|
|
|
|
if (!metric) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `primitives.typography.sizes.${size}`,
|
|
|
|
|
message: 'every canonical text size must be defined'
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
validateTextMetric(`primitives.typography.sizes.${size}`, metric, issues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [name, family] of Object.entries(typography.families)) {
|
|
|
|
|
if (family.family.trim() === '') {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `primitives.typography.families.${name}.family`,
|
|
|
|
|
message: 'font family name cannot be empty'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [name, style] of Object.entries(typography.styles)) {
|
|
|
|
|
const path = `primitives.typography.styles.${name}`
|
|
|
|
|
|
|
|
|
|
if (style.family !== undefined && !hasOwn(typography.families, style.family)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.family`,
|
|
|
|
|
message: `typography style points to missing family "${style.family}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
typeof style.weight === 'string' &&
|
|
|
|
|
style.weight.trim() !== '' &&
|
|
|
|
|
!hasOwn(typography.weights, style.weight)
|
|
|
|
|
) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.weight`,
|
|
|
|
|
message: `typography style points to missing weight "${style.weight}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validateTypographySizeValue(`${path}.size`, style.size, typography, issues)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateSizePrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const sizes = options.primitives.size
|
|
|
|
|
if (!sizes) return
|
|
|
|
|
|
|
|
|
|
for (const size of SIZE_PRIMITIVE_KEYS) {
|
|
|
|
|
if (!hasOwn(sizes, size)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `primitives.size.${size}`,
|
|
|
|
|
message: 'every canonical physical size must be defined'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [name, size] of Object.entries(sizes)) {
|
|
|
|
|
const path = `primitives.size.${name}`
|
|
|
|
|
|
|
|
|
|
if (!SIZE_PRIMITIVE_KEY_SET.has(name)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: 'unknown size primitive key'
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validateSizePrimitive(path, size, options, issues)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateSizePrimitive(
|
|
|
|
|
path: string,
|
|
|
|
|
size: SizePrimitive,
|
|
|
|
|
options: EidosConfig,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (!hasOwn(options.primitives.controlHeight ?? {}, size.controlHeight)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.controlHeight`,
|
|
|
|
|
message: `size primitive points to missing control height "${size.controlHeight}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const typography = options.primitives.typography
|
|
|
|
|
if (!TEXT_SIZE_SET.has(size.fontSize) || !hasOwn(typography?.sizes ?? {}, size.fontSize)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.fontSize`,
|
|
|
|
|
message: `size primitive points to missing font size "${size.fontSize}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasOwn(options.primitives.icon?.size ?? {}, size.iconSize)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.iconSize`,
|
|
|
|
|
message: `size primitive points to missing icon size "${size.iconSize}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasOwn(options.primitives.radius ?? {}, size.radius)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.radius`,
|
|
|
|
|
message: `size primitive points to missing radius "${size.radius}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validateNonEmptyCssValue(`${path}.paddingInline`, size.paddingInline, issues)
|
|
|
|
|
validateNonEmptyCssValue(`${path}.paddingBlock`, size.paddingBlock, issues)
|
|
|
|
|
validateNonEmptyCssValue(`${path}.gap`, size.gap, issues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateBorderPrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const border = options.primitives.border
|
|
|
|
|
if (!border) return
|
|
|
|
|
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.border.width',
|
|
|
|
|
border.width,
|
|
|
|
|
BORDER_WIDTH_KEYS,
|
|
|
|
|
BORDER_WIDTH_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.border.style',
|
|
|
|
|
border.style,
|
|
|
|
|
BORDER_STYLE_KEYS,
|
|
|
|
|
BORDER_STYLE_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
validateBorderDefault(
|
|
|
|
|
'primitives.border.defaultWidth',
|
|
|
|
|
border.defaultWidth,
|
|
|
|
|
border,
|
|
|
|
|
'width',
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateBorderDefault(
|
|
|
|
|
'primitives.border.defaultStyle',
|
|
|
|
|
border.defaultStyle,
|
|
|
|
|
border,
|
|
|
|
|
'style',
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateBorderDefault(
|
|
|
|
|
path: string,
|
|
|
|
|
value: string,
|
|
|
|
|
border: BorderPrimitiveSet,
|
|
|
|
|
field: 'width' | 'style',
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (hasOwn(border[field], value)) return
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: `border default points to missing ${field} "${value}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateLayoutPrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
const layout = options.primitives.layout
|
|
|
|
|
if (!layout) return
|
|
|
|
|
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.layout.containerWidth',
|
|
|
|
|
layout.containerWidth,
|
|
|
|
|
CONTAINER_WIDTH_KEYS,
|
|
|
|
|
CONTAINER_WIDTH_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.layout.contentWidth',
|
|
|
|
|
layout.contentWidth,
|
|
|
|
|
CONTENT_WIDTH_KEYS,
|
|
|
|
|
CONTENT_WIDTH_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
'primitives.layout.aspectRatio',
|
|
|
|
|
layout.aspectRatio,
|
|
|
|
|
ASPECT_RATIO_KEYS,
|
|
|
|
|
ASPECT_RATIO_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
validateNonEmptyCssValue(
|
|
|
|
|
'primitives.layout.containerPaddingInline',
|
|
|
|
|
layout.containerPaddingInline,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateThemeShadows(options: EidosConfig, issues: EidosValidationIssue[]): void {
|
|
|
|
|
for (const [themeId, theme] of Object.entries(options.themes ?? {})) {
|
|
|
|
|
validateRecordPrimitive(
|
|
|
|
|
`themes.${themeId}.shadow`,
|
|
|
|
|
theme.shadow,
|
|
|
|
|
SHADOW_KEYS,
|
|
|
|
|
SHADOW_KEY_SET,
|
|
|
|
|
issues
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateRecordPrimitive(
|
|
|
|
|
path: string,
|
|
|
|
|
record: Record<string, string | number> | undefined,
|
|
|
|
|
keys: readonly string[],
|
|
|
|
|
keySet: Set<string>,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (!record) return
|
|
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
if (!hasOwn(record, key)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.${key}`,
|
|
|
|
|
message: 'every canonical token key must be defined'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(record)) {
|
|
|
|
|
if (!keySet.has(key)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.${key}`,
|
|
|
|
|
message: 'unknown token key'
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
validateNonEmptyCssValue(`${path}.${key}`, String(value), issues)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateTypographySizeValue(
|
|
|
|
|
path: string,
|
|
|
|
|
value: unknown,
|
|
|
|
|
typography: TypographyPrimitiveSet,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (value === undefined) return
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
if (!TEXT_SIZE_SET.has(value) || !hasOwn(typography.sizes, value)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: `typography style points to missing text size "${value}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isTextMetric(value)) {
|
|
|
|
|
validateTextMetric(path, value, issues)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isPlainRecord(value)) {
|
|
|
|
|
for (const [key, item] of Object.entries(value)) {
|
|
|
|
|
if (!RESPONSIVE_KEY_SET.has(key)) {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.${key}`,
|
|
|
|
|
message: `unknown responsive breakpoint "${key}"`
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
validateTypographySizeValue(`${path}.${key}`, item, typography, issues)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: 'typography size must be a canonical size, metric, or responsive map'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateNonEmptyCssValue(
|
|
|
|
|
path: string,
|
|
|
|
|
value: string,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (value.trim() !== '') return
|
|
|
|
|
issues.push({
|
|
|
|
|
path,
|
|
|
|
|
message: 'CSS value cannot be empty'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateTextMetric(
|
|
|
|
|
path: string,
|
|
|
|
|
metric: TextMetric,
|
|
|
|
|
issues: EidosValidationIssue[]
|
|
|
|
|
): void {
|
|
|
|
|
if (metric.size.trim() === '') {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.size`,
|
|
|
|
|
message: 'text metric size cannot be empty'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (metric.lineHeight.trim() === '') {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.lineHeight`,
|
|
|
|
|
message: 'text metric lineHeight cannot be empty'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (metric.letterSpacing.trim() === '') {
|
|
|
|
|
issues.push({
|
|
|
|
|
path: `${path}.letterSpacing`,
|
|
|
|
|
message: 'text metric letterSpacing cannot be empty'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isTextMetric(value: unknown): value is TextMetric {
|
|
|
|
|
return (
|
|
|
|
|
isPlainRecord(value) &&
|
|
|
|
|
typeof value.size === 'string' &&
|
|
|
|
|
typeof value.lineHeight === 'string' &&
|
|
|
|
|
typeof value.letterSpacing === 'string'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRoleScaleName(
|
|
|
|
|
roleDef: string | ColorRoleDefinition | undefined
|
|
|
|
|
): string | undefined {
|
|
|
|
|
if (typeof roleDef === 'string') return roleDef
|
|
|
|
|
return roleDef?.scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasOwn<T extends object>(value: T, key: PropertyKey): boolean {
|
|
|
|
|
return Object.prototype.hasOwnProperty.call(value, key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clonePlain<T>(value: T): T {
|
|
|
|
|
if (typeof structuredClone === 'function') return structuredClone(value)
|
|
|
|
|
return JSON.parse(JSON.stringify(value)) as T
|
|
|
|
|
}
|