// cSpell:ignore rcap,vmin,svmin,lvmin,dvmin,cqmin,vmax,svmax,lvmax,dvmax,cqmax,currentcolor,oklab,oklch,prophoto,squircle,oldstyle,nums import Color from "colorjs.io"; import type { Angle, BorderSideWidth, BorderStyle, ColorOrAuto, CssColor, CustomProperty, Declaration, DimensionPercentageFor_LengthValue, EnvironmentVariable, FontSize, FontStyle, FontVariantCaps, FontWeight, GapValue, Gradient, GradientItemFor_DimensionPercentageFor_LengthValue, Length, LengthPercentageOrAuto, LengthValue, LineDirection, LineHeight, LineStyle, MaxSize, NumberOrPercentage, Scale, Size, Size2DFor_DimensionPercentageFor_LengthValue, Time, Token, TokenOrValue, Translate, UnresolvedColor, } from "lightningcss"; import { isStyleFunction } from "../utilities"; import type { StyleDescriptor, StyleFunction, StyleRule, } from "./compiler.types"; import { parseEasingFunction, parseIterationCount } from "./keyframes"; import { toRNProperty } from "./selectors"; import type { StylesheetBuilder } from "./stylesheet"; const CommaSeparator = Symbol("CommaSeparator"); type DeclarationType

= Extract< Declaration, { property: P } >; type Parser = ( declaration: Extract, builder: StylesheetBuilder, propertyName: string, // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => StyleDescriptor | void; const propertyRename: Record = { "background-image": "experimental_backgroundImage", }; const unparsedRuntimeParsing = new Set([ "animation", "border", "box-shadow", "line-height", "rotate", "scale", "text-shadow", "transform", "translate", ]); const parsers: { [K in Declaration["property"]]?: Parser; } = { "align-content": parseAlignContent, "align-items": parseAlignItems, "align-self": parseAlignSelf, "animation": addAnimationValue, "animation-delay": addAnimationValue, "animation-direction": addAnimationValue, "animation-duration": addAnimationValue, "animation-fill-mode": addAnimationValue, "animation-iteration-count": addAnimationValue, "animation-name": addAnimationValue, "animation-play-state": addAnimationValue, "animation-timing-function": addAnimationValue, "aspect-ratio": parseAspectRatio, "backface-visibility": parseBackfaceVisibility, "background-color": parseColorDeclaration, "background-image": parseBackgroundImage, "block-size": parseSizeDeclaration, "border": parseBorder, "border-block": parseBorderBlock, "border-block-color": parseBorderColor, "border-block-end": parseBorderBlockEnd, "border-block-end-color": parseColorDeclaration, "border-block-end-width": parseBorderSideWidthDeclaration, "border-block-start": parseBorderBlockStart, "border-block-start-color": parseColorDeclaration, "border-block-start-style": parseBorderStyleDeclaration, "border-block-start-width": parseBorderSideWidthDeclaration, "border-block-style": parseBorderBlockStyle, "border-block-width": parseBorderBlockWidth, "border-bottom": parseBorderSide, "border-bottom-color": parseColorDeclaration, "border-bottom-left-radius": parseSize2DDimensionPercentageDeclaration, "border-bottom-right-radius": parseSize2DDimensionPercentageDeclaration, "border-bottom-style": parseBorderStyleDeclaration, "border-bottom-width": parseBorderSideWidthDeclaration, "border-color": parseBorderColor, "border-end-end-radius": parseSize2DDimensionPercentageDeclaration, "border-end-start-radius": parseSize2DDimensionPercentageDeclaration, "border-inline": parseBorderInline, "border-inline-color": parseBorderColor, "border-inline-end": parseBorderInlineEnd, "border-inline-end-color": parseColorDeclaration, "border-inline-end-style": parseBorderInlineStyle, "border-inline-end-width": parseBorderSideWidthDeclaration, "border-inline-start": parseBorderInlineStart, "border-inline-start-color": parseColorDeclaration, "border-inline-start-style": parseBorderInlineStyle, "border-inline-start-width": parseBorderSideWidthDeclaration, "border-inline-style": parseBorderInlineStyle, "border-inline-width": parseBorderInlineWidth, "border-left": parseBorderSide, "border-left-color": parseColorDeclaration, "border-left-style": parseBorderStyleDeclaration, "border-left-width": parseBorderSideWidthDeclaration, "border-radius": parseBorderRadius, "border-right": parseBorderSide, "border-right-color": parseColorDeclaration, "border-right-style": parseBorderStyleDeclaration, "border-right-width": parseBorderSideWidthDeclaration, "border-start-end-radius": parseSize2DDimensionPercentageDeclaration, "border-start-start-radius": parseSize2DDimensionPercentageDeclaration, "border-style": parseBorderStyleDeclaration, "border-top": parseBorderSide, "border-top-color": parseColorDeclaration, "border-top-left-radius": parseSize2DDimensionPercentageDeclaration, "border-top-right-radius": parseSize2DDimensionPercentageDeclaration, "border-top-style": parseBorderStyleDeclaration, "border-top-width": parseBorderSideWidthDeclaration, "border-width": parseBorderWidth, "bottom": parseSizeWithAutoDeclaration, "box-shadow": parseBoxShadow, "box-sizing": parseBoxSizing, "caret-color": parseColorOrAutoDeclaration, "color": parseFontColorDeclaration, "column-gap": parseGap, "container": parseContainer, "container-name": parseContainerName, "container-type": parseContainerType, "display": parseDisplay, "direction": parseDirection, "fill": parseSVGPaint, "filter": parseFilter, "flex": parseFlex, "flex-basis": parseLengthPercentageDeclaration, "flex-direction": ({ value }) => value, "flex-flow": parseFlexFlow, "flex-grow": ({ value }) => value, "flex-shrink": ({ value }) => value, "flex-wrap": ({ value }) => value, "font": parseFont, "font-family": parseFontFamily, "font-size": parseFontSizeDeclaration, "font-style": parseFontStyleDeclaration, "font-variant-caps": parseFontVariantCapsDeclaration, "font-weight": parseFontWeightDeclaration, "gap": parseGap, "height": parseSizeWithAutoDeclaration, "inline-size": parseSizeWithAutoDeclaration, "inset": parseInset, "inset-block": parseInsetBlock, "inset-block-end": parseLengthPercentageDeclaration, "inset-block-start": parseLengthPercentageDeclaration, "inset-inline": parseInsetInline, "inset-inline-end": parseLengthPercentageDeclaration, "inset-inline-start": parseLengthPercentageDeclaration, "justify-content": parseJustifyContent, "left": parseSizeWithAutoDeclaration, "letter-spacing": parseLetterSpacing, "line-height": parseLineHeightDeclaration, "margin": parseMargin, "margin-block": parseMarginBlock, "margin-block-end": parseLengthPercentageOrAutoDeclaration, "margin-block-start": parseLengthPercentageOrAutoDeclaration, "margin-bottom": parseSizeWithAutoDeclaration, "margin-inline": parseMarginInline, "margin-inline-end": parseLengthPercentageOrAutoDeclaration, "margin-inline-start": parseLengthPercentageOrAutoDeclaration, "margin-left": parseSizeWithAutoDeclaration, "margin-right": parseSizeWithAutoDeclaration, "margin-top": parseSizeWithAutoDeclaration, "max-block-size": parseSizeDeclaration, "max-height": parseSizeDeclaration, "max-inline-size": parseSizeDeclaration, "max-width": parseSizeDeclaration, "min-block-size": parseSizeDeclaration, "min-height": parseSizeDeclaration, "min-inline-size": parseSizeDeclaration, "min-width": parseSizeDeclaration, "opacity": ({ value }) => round(value), "outline-color": parseColorDeclaration, "outline-style": parseOutlineStyle, "outline-width": parseBorderSideWidthDeclaration, "overflow": parseOverflow, "padding": parsePadding, "padding-block": parsePaddingBlock, "padding-block-end": parseLengthPercentageDeclaration, "padding-block-start": parseLengthPercentageDeclaration, "padding-bottom": parseSizeDeclaration, "padding-inline": parsePaddingInline, "padding-inline-end": parseLengthPercentageDeclaration, "padding-inline-start": parseLengthPercentageDeclaration, "padding-left": parseSizeDeclaration, "padding-right": parseSizeDeclaration, "padding-top": parseSizeDeclaration, "position": parsePosition, "right": parseSizeWithAutoDeclaration, "rotate": parseRotate, "row-gap": parseGap, "scale": parseScale, "stroke": parseSVGPaint, "stroke-width": parseLengthDeclaration, "text-align": parseTextAlign, "text-decoration": parseTextDecoration, "text-decoration-color": parseColorDeclaration, "text-decoration-line": parseTextDecorationLineDeclaration, "text-decoration-style": parseTextDecorationStyle, "text-shadow": parseTextShadow, "text-transform": ({ value }) => value.case, "top": parseSizeWithAutoDeclaration, "transform": parseTransform, "transition": addTransitionValue, "transition-delay": addTransitionValue, "transition-duration": addTransitionValue, "transition-property": addTransitionValue, "transition-timing-function": addTransitionValue, "translate": parseTranslate, "user-select": parseUserSelect, "vertical-align": parseVerticalAlign, "visibility": parseVisibility, "width": parseSizeWithAutoDeclaration, "z-index": parseZIndex, }; // This is missing LightningCSS types (parsers as Record)["pointer-events"] = parsePointerEvents as Parser; const validProperties = new Set(Object.keys(parsers)); export function parseDeclaration( declaration: Declaration, builder: StylesheetBuilder, ) { if ("vendorPrefix" in declaration && declaration.vendorPrefix.length) { return; } if ( "value" in declaration && typeof declaration.value === "object" && "vendorPrefix" in declaration.value && Array.isArray(declaration.value.vendorPrefix) && declaration.value.vendorPrefix.length ) { return; } if (declaration.property === "unparsed") { parseUnparsedDeclaration(declaration, builder); } else if (declaration.property === "custom") { parseCustomDeclaration(declaration, builder); } else { parseWithParser(declaration, builder); } } function parseWithParser(declaration: Declaration, builder: StylesheetBuilder) { if (declaration.property in parsers) { const parser = parsers[declaration.property] as Parser; builder.descriptorProperty = declaration.property; builder.setWarningProperty(declaration.property); const value = parser(declaration, builder, declaration.property); if (value !== undefined) { builder.addDescriptor( propertyRename[declaration.property] ?? declaration.property, value, ); } } else { builder.addWarning("property", declaration.property); } } function parseInsetBlock( { value }: DeclarationType<"inset-block">, builder: StylesheetBuilder, ) { builder.addShorthand("inset-block", { "inset-block-start": parseLengthPercentageOrAuto(value.blockStart, builder), "inset-block-end": parseLengthPercentageOrAuto(value.blockEnd, builder), }); } function parseInsetInline( { value }: DeclarationType<"inset-inline">, builder: StylesheetBuilder, ) { builder.addShorthand("inset-inline", { "inset-block-start": parseLengthPercentageOrAuto( value.inlineStart, builder, ), "inset-block-end": parseLengthPercentageOrAuto(value.inlineEnd, builder), }); } function parseInset( { value }: DeclarationType<"inset">, builder: StylesheetBuilder, ) { builder.addShorthand("inset", { top: parseLengthPercentageOrAuto(value.top, builder), bottom: parseLengthPercentageOrAuto(value.bottom, builder), left: parseLengthPercentageOrAuto(value.left, builder), right: parseLengthPercentageOrAuto(value.right, builder), }); } function parseBorderRadius( { value }: DeclarationType<"border-radius">, builder: StylesheetBuilder, ) { builder.addShorthand("border-radius", { "border-bottom-left-radius": parseSize2DDimensionPercentage( value.bottomLeft, builder, ), "border-bottom-right-radius": parseSize2DDimensionPercentage( value.bottomRight, builder, ), "border-top-left-radius": parseSize2DDimensionPercentage( value.topLeft, builder, ), "border-top-right-radius": parseSize2DDimensionPercentage( value.topRight, builder, ), }); } function parseBorderColor( declaration: DeclarationType< "border-color" | "border-block-color" | "border-inline-color" >, builder: StylesheetBuilder, ) { if (declaration.property === "border-color") { builder.addShorthand("border-color", { "border-top-color": parseColor(declaration.value.top, builder), "border-bottom-color": parseColor(declaration.value.bottom, builder), "border-left-color": parseColor(declaration.value.left, builder), "border-right-color": parseColor(declaration.value.right, builder), }); } else { const start = parseColor(declaration.value.start, builder); const end = parseColor(declaration.value.end, builder); if (start === end) { builder.addDescriptor(declaration.property, start); } else if (declaration.property === "border-block-color") { builder.addDescriptor("border-top-color", start); builder.addDescriptor("border-bottom-color", end); } else { builder.addDescriptor("border-left-color", start); builder.addDescriptor("border-right-color", end); } } } function parseBorderWidth( { value }: DeclarationType<"border-width">, builder: StylesheetBuilder, ) { builder.addShorthand("border-width", { "border-top-width": parseBorderSideWidth(value.top, builder), "border-bottom-width": parseBorderSideWidth(value.bottom, builder), "border-left-width": parseBorderSideWidth(value.left, builder), "border-right-width": parseBorderSideWidth(value.right, builder), }); } function parseBorder( { value }: DeclarationType<"border">, builder: StylesheetBuilder, ) { builder.addShorthand("border", { "border-width": parseBorderSideWidth(value.width, builder), "border-style": parseBorderStyle(value.style, builder), "border-color": parseColor(value.color, builder), }); } function parseBorderSide( { value, property, }: DeclarationType< "border-top" | "border-bottom" | "border-left" | "border-right" >, builder: StylesheetBuilder, ) { builder.addDescriptor(property + "-color", parseColor(value.color, builder)); builder.addDescriptor( property + "-width", parseBorderSideWidth(value.width, builder), ); } function parseBorderBlock( { value }: DeclarationType<"border-block">, builder: StylesheetBuilder, ) { builder.addDescriptor("border-block-color", parseColor(value.color, builder)); builder.addDescriptor( "border-block-width", parseBorderSideWidth(value.width, builder), ); builder.addDescriptor( "border-block-style", parseBorderStyle(value.style, builder), ); } function parseBorderBlockStart( { value }: DeclarationType<"border-block-start">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-block-start-color", parseColor(value.color, builder), ); builder.addDescriptor( "border-block-start-width", parseBorderSideWidth(value.width, builder), ); } function parseBorderBlockEnd( { value }: DeclarationType<"border-block-end">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-block-end-color", parseColor(value.color, builder), ); builder.addDescriptor( "border-block-end-width", parseBorderSideWidth(value.width, builder), ); } function parseBorderInline( { value }: DeclarationType<"border-inline">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-inline-color", parseColor(value.color, builder), ); builder.addDescriptor( "border-inline-width", parseBorderSideWidth(value.width, builder), ); builder.addDescriptor( "border-inline-style", parseBorderStyle(value.style, builder), ); } function parseBorderInlineStart( { value }: DeclarationType<"border-inline-start">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-inline-start-color", parseColor(value.color, builder), ); builder.addDescriptor( "border-inline-start-width", parseBorderSideWidth(value.width, builder), ); builder.addDescriptor( "border-inline-start-style", parseBorderStyle(value.style, builder), ); } function parseBorderInlineEnd( { value }: DeclarationType<"border-inline-end">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-inline-end-color", parseColor(value.color, builder), ); builder.addDescriptor( "border-inline-end-width", parseBorderSideWidth(value.width, builder), ); builder.addDescriptor( "border-inline-end-style", parseBorderStyle(value.style, builder), ); } export function parseBorderInlineWidth( declaration: DeclarationType<"border-inline-width">, builder: StylesheetBuilder, ) { builder.addDescriptor( "border-inline-width", parseBorderSideWidth(declaration.value.start, builder), ); } export function parseBorderInlineStyle( declaration: DeclarationType< | "border-inline-style" | "border-inline-start-style" | "border-inline-end-style" >, builder: StylesheetBuilder, ) { if (typeof declaration.value === "string") { builder.addDescriptor( declaration.property, parseBorderStyle(declaration.value, builder), ); } else if (declaration.value.start === declaration.value.end) { builder.addDescriptor( declaration.property, parseBorderStyle(declaration.value.start, builder), ); } else { builder.addDescriptor( "border-inline-start-style", parseBorderStyle(declaration.value.start, builder), ); builder.addDescriptor( "border-inline-end-style", parseBorderStyle(declaration.value.end, builder), ); } } function parseFlexFlow( { value }: DeclarationType<"flex-flow">, builder: StylesheetBuilder, ) { builder.addDescriptor("flexWrap", value.wrap); builder.addDescriptor("flexDirection", value.direction); } function parseFlex( { value }: DeclarationType<"flex">, builder: StylesheetBuilder, ) { builder.addDescriptor("flex-grow", value.grow); builder.addDescriptor("flex-shrink", value.shrink); builder.addDescriptor( "flex-basis", parseLengthPercentageOrAuto(value.basis, builder), ); } function parseMargin( { value }: DeclarationType<"margin">, builder: StylesheetBuilder, ) { builder.addShorthand("margin", { "margin-top": parseSize(value.top, builder, { allowAuto: true }), "margin-bottom": parseSize(value.bottom, builder, { allowAuto: true }), "margin-left": parseSize(value.left, builder, { allowAuto: true }), "margin-right": parseSize(value.right, builder, { allowAuto: true }), }); } function parseMarginBlock( { value }: DeclarationType<"margin-block">, builder: StylesheetBuilder, ) { builder.addShorthand("margin-block", { "margin-block-start": parseLengthPercentageOrAuto( value.blockStart, builder, { allowAuto: true }, ), "margin-block-end": parseLengthPercentageOrAuto(value.blockEnd, builder, { allowAuto: true, }), }); } function parseMarginInline( { value }: DeclarationType<"margin-inline">, builder: StylesheetBuilder, ) { builder.addShorthand("margin-inline", { "margin-inline-start": parseLengthPercentageOrAuto( value.inlineStart, builder, { allowAuto: true }, ), "margin-inline-end": parseLengthPercentageOrAuto(value.inlineEnd, builder, { allowAuto: true, }), }); } function parsePadding( { value }: DeclarationType<"padding">, builder: StylesheetBuilder, ) { builder.addShorthand("padding", { "padding-top": parseSize(value.top, builder), "padding-bottom": parseSize(value.bottom, builder), "padding-left": parseSize(value.left, builder), "padding-right": parseSize(value.right, builder), }); } function parsePaddingBlock( { value }: DeclarationType<"padding-block">, builder: StylesheetBuilder, ) { builder.addShorthand("padding-block", { "padding-block-start": parseLengthPercentageOrAuto( value.blockStart, builder, ), "padding-block-end": parseLengthPercentageOrAuto(value.blockEnd, builder), }); } function parsePaddingInline( { value }: DeclarationType<"padding-inline">, builder: StylesheetBuilder, ) { builder.addShorthand("padding-inline", { "padding-inline-start": parseLengthPercentageOrAuto( value.inlineStart, builder, ), "padding-inline-end": parseLengthPercentageOrAuto(value.inlineEnd, builder), }); } function parseFont( { value }: DeclarationType<"font">, builder: StylesheetBuilder, ) { builder.addDescriptor("font-family", value.family[0]); builder.addDescriptor( "line-height", parseLineHeight(value.lineHeight, builder), ); builder.addDescriptor("font-size", parseFontSize(value.size, builder)); builder.addDescriptor("font-style", parseFontStyle(value.style, builder)); builder.addDescriptor( "font-variant-caps", parseFontVariantCaps(value.variantCaps, builder), ); builder.addDescriptor("font-weight", parseFontWeight(value.weight, builder)); } function parseTransform( { value }: DeclarationType<"transform">, builder: StylesheetBuilder, ) { builder.addDescriptor("transform", [ {}, "transform", value.flatMap((t): StyleDescriptor[] => { switch (t.type) { case "perspective": return [[{}, "perspective", parseLength(t.value, builder)]]; case "translate": return [ [ {}, "translateX", parseLengthOrCoercePercentageToRuntime(t.value[0], builder), ], [ [ {}, "translateY", parseLengthOrCoercePercentageToRuntime(t.value[1], builder), ], ], ]; case "translateX": return [ [ {}, "translateX", parseLengthOrCoercePercentageToRuntime(t.value, builder), ], ]; case "translateY": return [ [ {}, "translateY", parseLengthOrCoercePercentageToRuntime(t.value, builder), ], ]; case "rotate": return [[{}, "rotate", parseAngle(t.value, builder)]]; case "rotateX": return [[{}, "rotateX", parseAngle(t.value, builder)]]; case "rotateY": return [[{}, "rotateY", parseAngle(t.value, builder)]]; case "rotateZ": return [[{}, "rotateZ", parseAngle(t.value, builder)]]; case "scale": return [ [{}, "scaleX", parseLength(t.value[0], builder)], [{}, "scaleY", parseLength(t.value[1], builder)], ]; case "scaleX": return [[{}, "scaleX", parseLength(t.value, builder)]]; case "scaleY": return [[{}, "scaleY", parseLength(t.value, builder)]]; case "skew": return [ [{}, "skewX", parseAngle(t.value[0], builder)], [{}, "skewY", parseAngle(t.value[1], builder)], ]; case "skewX": return [[{}, "skewX", parseAngle(t.value, builder)]]; case "skewY": return [[{}, "skewY", parseAngle(t.value, builder)]]; case "translateZ": case "translate3d": case "scaleZ": case "scale3d": case "rotate3d": case "matrix": case "matrix3d": return [[]]; } }), ]); return; } function parseTranslate( { value }: DeclarationType<"translate">, builder: StylesheetBuilder, ) { builder.addDescriptor("translateX", [ {}, "translateX", parseTranslateProp(value, "x", builder), ]); builder.addDescriptor("translateY", [ {}, "translateY", parseTranslateProp(value, "y", builder), ]); } function parseRotate( { value }: DeclarationType<"rotate">, builder: StylesheetBuilder, ) { if (value.x) { builder.addDescriptor("rotateX", [ {}, "rotateX", parseAngle(value.angle, builder), ]); } if (value.y) { builder.addDescriptor("rotateY", [ {}, "rotateY", parseAngle(value.angle, builder), ]); } if (value.z) { builder.addDescriptor("rotateZ", [ {}, "rotateZ", parseAngle(value.angle, builder), ]); } } function parseScale( { value }: DeclarationType<"scale">, builder: StylesheetBuilder, ) { builder.addDescriptor("scaleX", [ {}, "scaleX", parseScaleValue(value, "x", builder), ]); builder.addDescriptor("scaleY", [ {}, "scaleY", parseScaleValue(value, "y", builder), ]); } export function parseScaleValue( translate: Scale, prop: keyof Extract, builder: StylesheetBuilder, ): StyleDescriptor { if (translate === "none") { return 0; } return parseLength(translate[prop], builder); } function parseLetterSpacing( { value }: DeclarationType<"letter-spacing">, builder: StylesheetBuilder, ) { if (value.type === "normal") { return; } return parseLength(value.value, builder); } function parseTextDecoration( { value }: DeclarationType<"text-decoration">, builder: StylesheetBuilder, ) { builder.addDescriptor( "text-decoration-color", parseColor(value.color, builder), ); builder.addDescriptor( "text-decoration-line", parseTextDecorationLine(value.line, builder), ); } function parseZIndex( { value }: DeclarationType<"z-index">, builder: StylesheetBuilder, ): StyleDescriptor { if (value.type === "integer") { return parseLength(value.value, builder); } else { builder.addWarning("value", value.type); return; } } function parseContainerType( _declaration: DeclarationType<"container-type">, builder: StylesheetBuilder, ) { builder.addContainer(["___default___"]); return; } function parseContainerName( { value }: DeclarationType<"container-name">, builder: StylesheetBuilder, ) { builder.addContainer(value.type === "none" ? false : value.value); return; } function parseContainer( { value }: DeclarationType<"container">, builder: StylesheetBuilder, ) { builder.addContainer(value.name.type === "none" ? false : value.name.value); return; } export function parseUnparsedDeclaration( declaration: Extract, builder: StylesheetBuilder, ) { let property = declaration.value.propertyId.property; if (!(property in parsers)) { builder.addWarning("property", property); return; } builder.setWarningProperty(property); /** * React Native doesn't support all the logical properties */ const rename = propertyRename[property]; if (rename) { property = rename; } /** * Unparsed shorthand properties need to be parsed at runtime */ builder.descriptorProperty = property; if (unparsedRuntimeParsing.has(property)) { const args = parseUnparsed(declaration.value.value, builder, property); if (property === "animation") { builder.addDescriptor("animation", [{}, property, args]); } else { builder.addDescriptor(property, [{}, toRNProperty(property), args, 1]); } } else { const value = parseUnparsed(declaration.value.value, builder, property); builder.addDescriptor(property, value); if (property === "font-size") { builder.addDescriptor("--__rn-css-em", value); } if (property === "color") { if ( !isStyleFunction(value) || value[1] !== "var" || value[2] !== "-css-color" ) { builder.addDescriptor("--__rn-css-color", value); } } } } export function parseCustomDeclaration( declaration: Extract, builder: StylesheetBuilder, ) { const property = declaration.value.name; if (property === "-webkit-line-clamp") { builder.addDescriptor( property, parseUnparsed(declaration.value.value, builder, property), ); } else if (property === "-rn-ripple-style") { if ( parseUnparsed(declaration.value.value, builder, property) === "borderless" ) { builder.addDescriptor(property, true); } } else if (property === "-rn-ripple-layer") { if ( parseUnparsed(declaration.value.value, builder, property) === "foreground" ) { builder.addDescriptor(property, true); } } else if (property === "object-fit") { // https://github.com/parcel-bundler/lightningcss/issues/1046 parseObjectFit(declaration.value, builder); } else if (property === "object-position") { // https://github.com/parcel-bundler/lightningcss/issues/1047 parseObjectPosition(declaration.value, builder); } else if (property === "outline-offset") { // https://github.com/parcel-bundler/lightningcss/issues/1048 builder.addDescriptor( property, parseUnparsed(declaration.value.value, builder, property), ); } else if (property === "corner-shape") { parseCornerShape(declaration.value, builder); } else if ( validProperties.has(property) || property.startsWith("--") || property.startsWith("-rn-") ) { builder.addDescriptor( property, parseUnparsed(declaration.value.value, builder, property), ); } else { builder.addWarning("property", declaration.value.name); } } export function reduceParseUnparsed( tokenOrValues: TokenOrValue[], builder: StylesheetBuilder, property: string, allowAuto: boolean, ): StyleDescriptor { const result = tokenOrValues .map((tokenOrValue) => parseUnparsed(tokenOrValue, builder, property, allowAuto), ) .filter((v) => v !== undefined); if (result.length === 0) { return undefined; } let currentGroup: StyleDescriptor = []; let groups: StyleDescriptor[] = [currentGroup]; for (const value of result) { if ((value as unknown) === CommaSeparator) { currentGroup = []; groups.push(currentGroup); } else { currentGroup.push(value); } } // Groups are the tokens grouped together by comma location // If a group only has 1 item, it shouldn't be an array groups = groups.flatMap((group): StyleDescriptor[] => { if (!Array.isArray(group)) { return []; } if (group.length === 0) { return []; } else if (group.length === 1) { const first = group[0]; if (first === undefined) { return []; } else { return [first]; } } else if ( // This is a special case for values group.includes("/") && group.every((item) => typeof item === "string" && item === "/" ? item : typeof item === "number", ) ) { // eslint-disable-next-line @typescript-eslint/no-base-to-string return [group.join(" ")]; } else { return [group]; } }); return groups.length === 1 ? groups[0] : groups; } export function unparsedFunction( token: Extract, builder: StylesheetBuilder, property: string, allowAuto: boolean, ): StyleFunction { return [ {}, toRNProperty(token.value.name), reduceParseUnparsed(token.value.arguments, builder, property, allowAuto), ]; } /** * When the CSS cannot be parsed (often due to a runtime condition like a CSS variable) * This export function best efforts parsing it into a export function that we can evaluate at runtime */ export function parseUnparsed( tokenOrValue: | TokenOrValue | TokenOrValue[] | string | number | undefined | null, builder: StylesheetBuilder, property: string, allowAuto = allowAutoProperties.has(property), ): StyleDescriptor { if (tokenOrValue === undefined || tokenOrValue === null) { return; } if (typeof tokenOrValue === "string") { if (tokenOrValue === "true") { return true; } else if (tokenOrValue === "false") { return false; } else if (tokenOrValue === "currentcolor") { return [{}, "var", "__rn-css-color"] as const; } else { return tokenOrValue; } } if (typeof tokenOrValue === "number") { return round(tokenOrValue); } if (Array.isArray(tokenOrValue)) { const args = reduceParseUnparsed( tokenOrValue, builder, property, allowAuto, ); if (!args) return; if (Array.isArray(args) && args.length === 1) { return args[0]; } else if ( (property === "filter" || property === "transform") && isStyleFunction(args) ) { return [args]; } else { return args; } } switch (tokenOrValue.type) { case "unresolved-color": { return parseUnresolvedColor( tokenOrValue.value, builder, property, allowAuto, ); } case "var": { let args: StyleDescriptor = tokenOrValue.value.name.ident.slice(2); const fallback = parseUnparsed( tokenOrValue.value.fallback, builder, property, allowAuto, ); if (fallback !== undefined) { args = [args, fallback]; } return [{}, "var", args, 1]; } case "function": { switch (tokenOrValue.value.name) { case "blur": case "brightness": case "contrast": case "cubic-bezier": case "drop-shadow": case "fontScale": case "getPixelSizeForLayoutSize": case "grayscale": case "hsl": case "hsla": case "hue-rotate": case "invert": case "opacity": case "pixelScale": case "platformColor": case "rgb": case "rgba": case "rotate": case "rotateX": case "rotateY": case "roundToNearestPixel": case "saturate": case "scale": case "scaleX": case "scaleY": case "sepia": case "shadow": case "skewX": case "skewY": case "steps": case "translate": case "translateX": case "translateY": return unparsedFunction(tokenOrValue, builder, property, allowAuto); case "linear-gradient": case "radial-gradient": // These are special as React Native requires the '-' in their name return [ {}, tokenOrValue.value.name, reduceParseUnparsed( tokenOrValue.value.arguments, builder, property, allowAuto, ), ]; case "hairlineWidth": return [{}, tokenOrValue.value.name, []]; case "calc": case "max": case "min": case "clamp": return parseCalcFn( tokenOrValue.value.name, tokenOrValue.value.arguments, builder, property, ); case "color-mix": return parseColorMix(tokenOrValue.value.arguments, builder, property); default: { builder.addWarning("value", `${tokenOrValue.value.name}()`); return; } } } case "length": return parseLength(tokenOrValue.value, builder); case "angle": return parseAngle(tokenOrValue.value, builder); case "token": switch (tokenOrValue.value.type) { case "string": case "ident": { const value = tokenOrValue.value.value; if (!allowAuto && value === "auto") { builder.addWarning("value", value); return; } if (value === "inherit" || value === "initial") { builder.addWarning("value", value); return; } else if (value === "currentcolor") { return [{}, "var", "__rn-css-color"] as const; } if (value === "true") { return true; } else if (value === "false") { return false; } else if (value === "infinity") { return Number.MAX_SAFE_INTEGER; } else { return value; } } case "number": { return round(tokenOrValue.value.value); } case "function": builder.addWarning("value", tokenOrValue.value.value); return; case "percentage": return `${round(tokenOrValue.value.value * 100)}%`; case "dimension": return parseDimension(tokenOrValue.value, builder); case "comma": return CommaSeparator as unknown as StyleDescriptor; case "delim": { if (tokenOrValue.value.value === "/") { return tokenOrValue.value.value; } return; } case "at-keyword": case "hash": case "id-hash": case "unquoted-url": case "white-space": case "comment": case "colon": case "semicolon": case "include-match": case "dash-match": case "prefix-match": case "suffix-match": case "substring-match": case "cdo": case "cdc": case "parenthesis-block": case "square-bracket-block": case "curly-bracket-block": case "bad-url": case "bad-string": case "close-parenthesis": case "close-square-bracket": case "close-curly-bracket": return; default: { tokenOrValue.value satisfies never; return; } } case "color": return parseColor(tokenOrValue.value, builder); case "env": return parseEnv(tokenOrValue.value, builder); case "time": return parseTime(tokenOrValue.value); case "url": case "resolution": case "dashed-ident": case "animation-name": return; default: { tokenOrValue satisfies never; } } return; } export function parseLengthDeclaration( declaration: { value: | number | Length | DimensionPercentageFor_LengthValue | NumberOrPercentage | LengthValue; }, builder: StylesheetBuilder, ) { return parseLength(declaration.value, builder); } export function parseLength( length: | number | Length | DimensionPercentageFor_LengthValue | NumberOrPercentage | LengthValue, builder: StylesheetBuilder, ): StyleDescriptor { const { inlineRem = 14 } = builder.getOptions(); if (typeof length === "number") { return round(length); } if ("unit" in length) { switch (length.unit) { case "px": { if (length.value === Infinity) { return Number.MAX_SAFE_INTEGER; } else if (length.value === -Infinity) { return Number.MIN_SAFE_INTEGER; } else { // Normalize large values to safe integers, e.g. `calc(infinity * 1px)` const value = Math.max( Math.min(length.value, Number.MAX_SAFE_INTEGER), Number.MIN_SAFE_INTEGER, ); return round(value); } } case "rem": if (typeof inlineRem === "number") { return length.value * inlineRem; } else { return [{}, "rem", round(length.value)]; } case "vw": case "vh": case "em": return [{}, length.unit, round(length.value), 1]; case "in": case "cm": case "mm": case "q": case "pt": case "pc": case "ex": case "rex": case "ch": case "rch": case "cap": case "rcap": case "ic": case "ric": case "lh": case "rlh": case "lvw": case "svw": case "dvw": case "cqw": case "lvh": case "svh": case "dvh": case "cqh": case "vi": case "svi": case "lvi": case "dvi": case "cqi": case "vb": case "svb": case "lvb": case "dvb": case "cqb": case "vmin": case "svmin": case "lvmin": case "dvmin": case "cqmin": case "vmax": case "svmax": case "lvmax": case "dvmax": case "cqmax": builder.addWarning("value", `${length.value}${length.unit}`); return undefined; default: { length.unit satisfies never; } } } else { switch (length.type) { case "calc": { // TODO: Add the calc polyfill return undefined; } case "number": { return round(length.value); } case "percentage": { return `${round(length.value * 100)}%`; } case "dimension": case "value": { return parseLength(length.value, builder); } } } return; } export function parseAngle(angle: Angle | number, builder: StylesheetBuilder) { if (typeof angle === "number") { return `${angle}deg`; } switch (angle.type) { case "deg": case "rad": return `${angle.value}${angle.type}`; default: builder.addWarning("value", `${angle.value} ${angle.type}`); return undefined; } } export function parseSizeDeclaration( declaration: { value: Size | MaxSize }, builder: StylesheetBuilder, ) { return parseSize(declaration.value, builder); } export function parseSizeWithAutoDeclaration( declaration: { value: Size | MaxSize }, builder: StylesheetBuilder, ) { return parseSize(declaration.value, builder, { allowAuto: true }); } export function parsePointerEvents( { value }: { value: string }, builder: StylesheetBuilder, ) { switch (value) { case "none": case "box-none": case "box-only": case "auto": return value; case "visible": case "visiblePainted": case "visibleFill": case "visibleStroke": case "painted": case "fill": case "stroke": builder.addWarning("value", value); } return; } export function parseSize( size: Size | MaxSize, builder: StylesheetBuilder, options?: { allowAuto?: boolean }, ): StyleDescriptor; export function parseSize( size: Size | MaxSize, builder: StylesheetBuilder, property: string, options?: { allowAuto?: boolean }, ): StyleDescriptor; export function parseSize( size: Size | MaxSize, builder: StylesheetBuilder, options?: string | { allowAuto?: boolean }, { allowAuto = false } = {}, ) { allowAuto = (typeof options === "object" ? options.allowAuto : allowAuto) ?? false; switch (size.type) { case "length-percentage": return parseLength(size.value, builder); case "none": return size.type; case "auto": if (allowAuto) { return size.type; } else { builder.addWarning("value", size.type); return undefined; } case "min-content": case "max-content": case "fit-content": case "fit-content-function": case "stretch": case "contain": builder.addWarning("value", size.type); return undefined; default: { size satisfies never; } } return; } export function parseColorOrAutoDeclaration( { value }: { value: ColorOrAuto }, builder: StylesheetBuilder, ) { if (value.type === "auto") { builder.addWarning("value", `Invalid color value ${value.type}`); return; } else { return parseColor(value.value, builder); } } export function parseFontColorDeclaration( declaration: Extract, builder: StylesheetBuilder, ) { parseColorDeclaration(declaration, builder); if ( typeof declaration.value !== "object" || declaration.value.type !== "currentcolor" ) { builder.addDescriptor( "--__rn-css-color", parseColor(declaration.value, builder), ); } } export function parseColorDeclaration( declaration: Extract, builder: StylesheetBuilder, ) { builder.addDescriptor( declaration.property, parseColor(declaration.value, builder), ); } export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) { if (typeof cssColor === "string") { if (namedColors.has(cssColor)) { return cssColor; } return; } let color: Color | undefined; const { hexColors = true, colorPrecision } = builder.getOptions(); switch (cssColor.type) { case "currentcolor": return [{}, "var", "__rn-css-color"] as const; case "light-dark": { const extraRule: StyleRule = { s: [], m: [["=", "prefers-color-scheme", "dark"]], }; builder.addUnnamedDescriptor( parseColor(cssColor.dark, builder), false, extraRule, ); builder.addExtraRule(extraRule); return parseColor(cssColor.light, builder); } case "rgb": { color = new Color({ space: "sRGB", coords: [cssColor.r / 255, cssColor.g / 255, cssColor.b / 255], alpha: cssColor.alpha, }); break; } case "hsl": color = new Color({ space: cssColor.type, coords: [cssColor.h, cssColor.s, cssColor.l], alpha: cssColor.alpha, }); break; case "hwb": color = new Color({ space: cssColor.type, coords: [cssColor.h, cssColor.w, cssColor.b], alpha: cssColor.alpha, }); break; case "lab": color = new Color({ space: cssColor.type, coords: [cssColor.l, cssColor.a, cssColor.b], alpha: cssColor.alpha, }); break; case "lch": color = new Color({ space: cssColor.type, coords: [cssColor.l, cssColor.c, cssColor.h], alpha: cssColor.alpha, }); break; case "oklab": color = new Color({ space: cssColor.type, coords: [cssColor.l, cssColor.a, cssColor.b], alpha: cssColor.alpha, }); break; case "oklch": color = new Color({ space: cssColor.type, coords: [cssColor.l, cssColor.c, cssColor.h], alpha: cssColor.alpha, }); break; case "srgb": color = new Color({ space: cssColor.type, coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "srgb-linear": color = new Color({ space: cssColor.type, coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "display-p3": color = new Color({ space: "p3", coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "a98-rgb": color = new Color({ space: "a98rgb", coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "prophoto-rgb": color = new Color({ space: "prophoto", coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "rec2020": color = new Color({ space: cssColor.type, coords: [cssColor.r, cssColor.g, cssColor.b], alpha: cssColor.alpha, }); break; case "xyz-d50": color = new Color({ space: cssColor.type, coords: [cssColor.x, cssColor.y, cssColor.z], alpha: cssColor.alpha, }); break; case "xyz-d65": color = new Color({ space: cssColor.type, coords: [cssColor.x, cssColor.y, cssColor.z], alpha: cssColor.alpha, }); break; default: { cssColor satisfies never; } } if (!hexColors || colorPrecision) { return color?.toString({ precision: colorPrecision ?? 3 }); } else { return color?.toString({ format: "hex" }); } } export function parseLengthPercentageDeclaration( value: { value: LengthPercentageOrAuto }, builder: StylesheetBuilder, ) { return parseLengthPercentageOrAuto(value.value, builder); } export function parseLengthPercentageOrAutoDeclaration( value: { value: LengthPercentageOrAuto }, builder: StylesheetBuilder, ) { return parseLengthPercentageOrAuto(value.value, builder, { allowAuto: true }); } export function parseLengthPercentageOrAuto( lengthPercentageOrAuto: LengthPercentageOrAuto, builder: StylesheetBuilder, { allowAuto = false } = {}, ) { switch (lengthPercentageOrAuto.type) { case "auto": if (allowAuto) { return lengthPercentageOrAuto.type; } else { builder.addWarning("value", lengthPercentageOrAuto.type); return undefined; } case "length-percentage": return parseLength(lengthPercentageOrAuto.value, builder); default: { lengthPercentageOrAuto satisfies never; } } return; } export function parseJustifyContent( declaration: DeclarationType<"justify-content">, builder: StylesheetBuilder, ) { const allowed = new Set([ "flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", ]); let value: string | undefined; switch (declaration.value.type) { case "normal": case "left": case "right": value = declaration.value.type; break; case "content-distribution": case "content-position": value = declaration.value.value; break; default: { declaration.value satisfies never; } } if (value && !allowed.has(value)) { builder.addWarning("value", value); return; } return value; } export function parseAlignContent( declaration: DeclarationType<"align-content">, builder: StylesheetBuilder, ) { const allowed = new Set([ "flex-start", "flex-end", "center", "stretch", "space-between", "space-around", "space-evenly", ]); let value: string | undefined; switch (declaration.value.type) { case "normal": case "baseline-position": value = declaration.value.type; break; case "content-distribution": case "content-position": value = declaration.value.value; break; default: { declaration.value satisfies never; } } if (value && !allowed.has(value)) { builder.addWarning("value", value); return; } return value; } export function parseAlignItems( alignItems: DeclarationType<"align-items">, builder: StylesheetBuilder, ) { const allowed = new Set([ "auto", "flex-start", "flex-end", "center", "stretch", "baseline", ]); let value: string | undefined; switch (alignItems.value.type) { case "normal": value = "auto"; break; case "stretch": value = alignItems.value.type; break; case "baseline-position": value = "baseline"; break; case "self-position": value = alignItems.value.value; break; default: { alignItems.value satisfies never; } } if (value && !allowed.has(value)) { builder.addWarning("value", value); return; } return value; } export function parseAlignSelf( alignSelf: DeclarationType<"align-self">, builder: StylesheetBuilder, ) { const allowed = new Set([ "auto", "flex-start", "flex-end", "center", "stretch", "baseline", ]); let value: string | undefined; switch (alignSelf.value.type) { case "normal": case "auto": value = "auto"; break; case "stretch": value = alignSelf.value.type; break; case "baseline-position": value = "baseline"; break; case "self-position": value = alignSelf.value.value; break; default: { alignSelf.value satisfies never; } } if (value && !allowed.has(value)) { builder.addWarning("value", value); return; } return value; } export function parseFontWeightDeclaration( declaration: DeclarationType<"font-weight">, builder: StylesheetBuilder, ) { return parseFontWeight(declaration.value, builder); } export function parseFontWeight( fontWeight: FontWeight, builder: StylesheetBuilder, ) { switch (fontWeight.type) { case "absolute": if (fontWeight.value.type === "weight") { return fontWeight.value.value; } else { return fontWeight.value.type; } case "bolder": case "lighter": builder.addWarning("value", fontWeight.type); return; default: { fontWeight satisfies never; } } return; } export function parseTextShadow( declaration: DeclarationType<"text-shadow">, builder: StylesheetBuilder, ) { const [textShadow] = declaration.value; if (!textShadow) { return; } builder.addDescriptor( "textShadowColor", parseColor(textShadow.color, builder), ); builder.addDescriptor( "&.textShadowOffset.width", parseLength(textShadow.xOffset, builder), ); builder.addDescriptor( "&.textShadowOffset.height", parseLength(textShadow.yOffset, builder), ); builder.addDescriptor( "textShadowRadius", parseLength(textShadow.blur, builder), ); } export function parseTextDecorationStyle( declaration: DeclarationType<"text-decoration-style">, builder: StylesheetBuilder, ) { const allowed = new Set(["solid", "double", "dotted", "dashed"]); if (allowed.has(declaration.value)) { return declaration.value; } builder.addWarning("value", declaration.value); return; } export function parseTextDecorationLineDeclaration( declaration: DeclarationType<"text-decoration-line">, builder: StylesheetBuilder, ) { return parseTextDecorationLine(declaration.value, builder); } export function parseTextDecorationLine( value: DeclarationType<"text-decoration-line">["value"], builder: StylesheetBuilder, ) { if (!Array.isArray(value)) { if (value === "none") { return value; } builder.addWarning("value", value); return; } const set = new Set(value); if (set.has("underline")) { if (set.has("line-through")) { return "underline line-through"; } else { return "underline"; } } else if (set.has("line-through")) { return "line-through"; } builder.addWarning("value", value.join(" ")); return undefined; } export function parsePosition( { value }: DeclarationType<"position">, builder: StylesheetBuilder, ) { if ( value.type === "absolute" || value.type === "relative" || value.type === "static" ) { return value.type; } builder.addWarning("value", value.type); return; } export function parseOverflow( { value }: DeclarationType<"overflow">, builder: StylesheetBuilder, ) { const allowed = new Set(["visible", "hidden"]); if (allowed.has(value.x)) { return value.x; } builder.addWarning("value", value.x); return undefined; } export function parseBorderStyleDeclaration( declaration: Extract< DeclarationType, { value: LineStyle | BorderStyle } >, builder: StylesheetBuilder, ) { return parseBorderStyle(declaration.value, builder); } export function parseBorderStyle( value: BorderStyle | LineStyle, builder: StylesheetBuilder, ) { const allowed = new Set(["solid", "dotted", "dashed"]); if (typeof value === "string") { if (allowed.has(value)) { return value; } else { builder.addWarning("value", value); return undefined; } } else if ( value.top === value.bottom && value.top === value.left && value.top === value.right && allowed.has(value.top) ) { return value.top; } builder.addWarning("value", value.top); return undefined; } export function parseBorderBlockWidth( declaration: DeclarationType<"border-block-width">, builder: StylesheetBuilder, ) { const start = parseBorderSideWidth(declaration.value.start, builder); const end = parseBorderSideWidth(declaration.value.end, builder); if (start === end) { builder.addDescriptor("border-block-width", start); } else { builder.addDescriptor("border-block-start-width", start); builder.addDescriptor("border-block-end-width", end); } } function parseBorderBlockStyle( declaration: DeclarationType<"border-block-style">, builder: StylesheetBuilder, ) { const start = parseBorderStyle(declaration.value.start, builder); const end = parseBorderStyle(declaration.value.end, builder); if (start == end) { builder.addDescriptor("border-block-style", start); } else { builder.addDescriptor("border-block-start-style", start); builder.addDescriptor("border-block-end-style", end); } } export function parseBorderSideWidthDeclaration( declaration: Extract, builder: StylesheetBuilder, ) { builder.addDescriptor( declaration.property, parseBorderSideWidth(declaration.value, builder), ); } export function parseBorderSideWidth( value: BorderSideWidth, builder: StylesheetBuilder, ) { if (value.type === "length") { return parseLength(value.value, builder); } builder.addWarning("value", value.type); return undefined; } export function parseVerticalAlign( { value }: DeclarationType<"vertical-align">, builder: StylesheetBuilder, ) { if (value.type === "length") { return undefined; } const allowed = new Set(["auto", "top", "bottom", "middle"]); if (allowed.has(value.value)) { return value.value; } builder.addWarning("value", value.value); return undefined; } function parseFontFamily({ value }: DeclarationType<"font-family">) { // React Native only allows one font family - better hope this is the right one :) return value[0]; } export function parseLineHeightDeclaration( declaration: DeclarationType<"line-height">, builder: StylesheetBuilder, ) { builder.addDescriptor("line-height", [ {}, "lineHeight", [parseLineHeight(declaration.value, builder)], 1, ]); } export function parseLineHeight( value: LineHeight, builder: StylesheetBuilder, ): StyleDescriptor { switch (value.type) { case "normal": return undefined; case "number": return [{}, "em", [value.value], 1]; case "length": { const length = value.value; switch (length.type) { case "dimension": return parseLength(length, builder); case "percentage": case "calc": builder.addWarning( "style", "line-height", typeof length.value === "number" ? length.value : JSON.stringify(length.value), ); return; default: { length satisfies never; } } return; } default: { value satisfies never; } } return; } export function parseFontSizeDeclaration( declaration: DeclarationType<"font-size">, builder: StylesheetBuilder, ) { const fontSize = parseFontSize(declaration.value, builder); builder.addDescriptor("fontSize", fontSize); builder.addDescriptor("--__rn-css-em", fontSize); } export function parseFontSize(value: FontSize, builder: StylesheetBuilder) { switch (value.type) { case "length": return parseLength(value.value, builder); case "absolute": case "relative": builder.addWarning("value", value.value); return undefined; default: { value satisfies never; } } return; } export function parseFontStyleDeclaration( declaration: DeclarationType<"font-style">, builder: StylesheetBuilder, ) { return parseFontStyle(declaration.value, builder); } export function parseFontStyle(value: FontStyle, builder: StylesheetBuilder) { switch (value.type) { case "normal": case "italic": return value.type; case "oblique": builder.addWarning("value", value.type); return undefined; default: { value satisfies never; } } return; } export function parseFontVariantCapsDeclaration( declaration: DeclarationType<"font-variant-caps">, builder: StylesheetBuilder, ) { return parseFontVariantCaps(declaration.value, builder); } export function parseFontVariantCaps( value: FontVariantCaps, builder: StylesheetBuilder, ) { const allowed = new Set([ "small-caps", "oldstyle-nums", "lining-nums", "tabular-nums", "proportional-nums", ]); if (allowed.has(value)) { return value; } builder.addWarning("value", value); return undefined; } export function parseLengthOrCoercePercentageToRuntime( value: Length | DimensionPercentageFor_LengthValue | NumberOrPercentage, builder: StylesheetBuilder, ): StyleDescriptor { return parseLength(value, builder); } export function parseGap( declaration: DeclarationType<"gap" | "column-gap" | "row-gap">, builder: StylesheetBuilder, ) { if ("column" in declaration.value) { const row = parseGapValue(declaration.value.row, builder); const column = parseGapValue(declaration.value.column, builder); if (row !== column) { builder.addDescriptor("row-gap", row); builder.addDescriptor("column-gap", column); } else { builder.addDescriptor("gap", row); } } else if (declaration.value.type === "normal") { builder.addWarning("value", declaration.value.type); } else { return parseLength(declaration.value.value, builder); } return; } function parseGapValue( value: GapValue, builder: StylesheetBuilder, ): StyleDescriptor { if (value.type === "normal") { return; } else { return parseLength(value.value, builder); } } export function parseTextAlign( { value }: DeclarationType<"text-align">, builder: StylesheetBuilder, ) { const allowed = new Set(["auto", "left", "right", "center", "justify"]); if (allowed.has(value)) { return value; } builder.addWarning("value", value); return undefined; } export function parseBoxShadow( { value }: DeclarationType<"box-shadow">, builder: StylesheetBuilder, ) { for (const [index, shadow] of value.entries()) { builder.addDescriptor( `&.boxShadow.[${index}].color`, parseColor(shadow.color, builder), ); builder.addDescriptor( `&.boxShadow.[${index}].offsetX`, parseLength(shadow.xOffset, builder), ); builder.addDescriptor( `&.boxShadow.[${index}].offsetY`, parseLength(shadow.yOffset, builder), ); builder.addDescriptor( `&.boxShadow.[${index}].blurRadius`, parseLength(shadow.blur, builder), ); builder.addDescriptor( `&.boxShadow.[${index}].spreadDistance`, parseLength(shadow.spread, builder), ); builder.addDescriptor( `&.boxShadow.[${index}].inset`, shadow.inset ? true : undefined, ); } } export function parseBoxSizing( declaration: DeclarationType<"box-sizing">, builder: StylesheetBuilder, ) { if (["border-box", "content-box"].includes(declaration.value)) { return declaration.value; } builder.addWarning("value", declaration.value); return undefined; } export function parseDisplay( { value }: DeclarationType<"display">, builder: StylesheetBuilder, ) { if (value.type === "keyword") { if (value.value === "none" || value.value === "contents") { return value.value; } else { builder.addWarning("value", value.value); return; } } else { if (value.outside === "block") { switch (value.inside.type) { case "flow": if (value.isListItem) { builder.addWarning("value", "list-item"); } else { builder.addWarning("value", "block"); } return; case "flex": return value.inside.type; case "flow-root": case "table": case "box": case "grid": case "ruby": builder.addWarning("value", value.inside.type); return; } } else { switch (value.inside.type) { case "flow": builder.addWarning("value", "inline"); return; case "flow-root": builder.addWarning("value", "inline-block"); return; case "table": builder.addWarning("value", "inline-table"); return; case "flex": builder.addWarning("value", "inline-flex"); return; case "box": case "grid": builder.addWarning("value", "inline-grid"); return; case "ruby": builder.addWarning("value", value.inside.type); return; } } } } export function parseDirection( declaration: DeclarationType<"direction">, builder: StylesheetBuilder, ) { if (["ltr", "rtl"].includes(declaration.value)) { builder.addDescriptor("direction", declaration.value); builder.addDescriptor("--__rn-css-direction", declaration.value); } builder.addWarning("value", declaration.value); return; } export function parseAspectRatio({ value, }: DeclarationType<"aspect-ratio">): StyleDescriptor { if (!value.ratio) { return; } else if (value.auto) { return "auto"; } else { const [width, height] = value.ratio; if (width === height) { return 1; } else { return `${width}/${height}`; } } } export function parseBackfaceVisibility( { value }: DeclarationType<"backface-visibility">, builder: StylesheetBuilder, ): StyleDescriptor { if (["visible", "hidden"].includes(value)) { return value; } else { builder.addWarning("value", value); return; } } export function parseDimension( { unit, value }: Extract, builder: StylesheetBuilder, ): StyleDescriptor { switch (unit) { case "px": if (value === Infinity) { return 9999; } else { return value; } case "%": return `${value}%`; case "rnh": case "rnw": return [{}, unit, [value / 100], 1]; default: { builder.addWarning("value", `${value}${unit}`); return; } } } export function parseUserSelect( { value }: DeclarationType<"user-select">, builder: StylesheetBuilder, ) { const allowed = ["auto", "text", "none", "contain", "all"]; if (allowed.includes(value)) { return value; } else { builder.addWarning("value", value); return; } } export function parseSVGPaint( { value, property }: DeclarationType<"fill" | "stroke">, builder: StylesheetBuilder, ) { let parsedValue: StyleDescriptor | undefined; if (value.type === "none") { parsedValue = "transparent"; } else if (value.type === "color") { parsedValue = parseColor(value.value, builder); } else { return; } builder.addDescriptor(property, parsedValue); } export function round(number: number) { return Math.round((number + Number.EPSILON) * 10000) / 10000; } export function parseDimensionPercentageFor_LengthValue( value: DimensionPercentageFor_LengthValue, builder: StylesheetBuilder, ) { if (value.type === "calc") { return undefined; } else if (value.type === "percentage") { return `${value.value}%`; } else { return parseLength(value.value, builder); } } const allowAutoProperties = new Set(["pointer-events"]); export function parseEnv( value: EnvironmentVariable, builder: StylesheetBuilder, ): StyleFunction | undefined { switch (value.name.type) { case "ua": switch (value.name.value) { case "safe-area-inset-top": case "safe-area-inset-right": case "safe-area-inset-bottom": case "safe-area-inset-left": { const fallback = parseUnparsed( value.fallback, builder, value.name.value, ); return fallback ? [{}, "var", [`react-native-css-${value.name.value}`, fallback], 1] : [{}, "var", [`react-native-css-${value.name.value}`], 1]; } case "viewport-segment-width": case "viewport-segment-height": case "viewport-segment-top": case "viewport-segment-left": case "viewport-segment-bottom": case "viewport-segment-right": } break; case "custom": case "unknown": } return; } export function parseCalcFn( name: string, tokens: TokenOrValue[], builder: StylesheetBuilder, property: string, ): StyleDescriptor { const args = parseCalcArguments(tokens, builder, property); if (args) { return [{}, name, args]; } return; } export function parseColorMix( tokens: TokenOrValue[], builder: StylesheetBuilder, property: string, ): StyleDescriptor { const [inToken, whitespace, colorSpace, comma, ...rest] = tokens; if ( typeof inToken !== "object" || inToken.type !== "token" || inToken.value.type !== "ident" || inToken.value.value !== "in" ) { return; } if ( typeof whitespace !== "object" || whitespace.type !== "token" || whitespace.value.type !== "white-space" ) { return; } if ( typeof comma !== "object" || comma.type !== "token" || comma.value.type !== "comma" ) { return; } const colorSpaceArg = parseUnparsed(colorSpace, builder, property); if (typeof colorSpaceArg !== "string") { return; } let nextToken = rest.shift(); const leftColorArg = parseUnparsed(nextToken, builder, property); if (!leftColorArg) { return; } nextToken = rest.shift(); let leftColorPercentage: StyleDescriptor | undefined; if (nextToken?.type !== "token" || nextToken.value.type !== "comma") { leftColorPercentage = parseUnparsed(nextToken, builder, property); nextToken = rest.shift(); } if ( typeof nextToken !== "object" || nextToken.type !== "token" || nextToken.value.type !== "comma" ) { return; } nextToken = rest.shift(); const rightColorArg = parseUnparsed(nextToken, builder, property); if (rightColorArg === "transparent") { // Ignore the rest, treat as single color with alpha return [{}, "colorMix", [colorSpaceArg, leftColorArg, leftColorPercentage]]; } nextToken = rest.shift(); let rightColorPercentage: StyleDescriptor | undefined; if (nextToken?.type !== "token" || nextToken.value.type !== "comma") { rightColorPercentage = parseUnparsed(nextToken, builder, property); nextToken = rest.shift(); } // We should have expired all tokens now if (nextToken) { return; } return [ {}, "colorMix", [ colorSpaceArg, leftColorArg, leftColorPercentage, rightColorArg, rightColorPercentage, ], ]; } export function parseCalcArguments( [...args]: TokenOrValue[], builder: StylesheetBuilder, property: string, ) { const parsed: StyleDescriptor[] = []; for (const arg of args) { switch (arg.type) { case "env": { parsed.push(parseEnv(arg.value, builder)); break; } case "var": case "function": case "unresolved-color": { const value = parseUnparsed(arg, builder, property); if (value === undefined) { return undefined; } parsed.push(value); break; } case "length": { const value = parseLength(arg.value, builder); if (value !== undefined) { parsed.push(value); } break; } case "color": case "url": case "angle": case "time": case "resolution": case "dashed-ident": break; case "token": switch (arg.value.type) { case "delim": switch (arg.value.value) { case "+": case "-": case "*": case "/": parsed.push(arg.value.value); break; } break; case "percentage": parsed.push(`${round(arg.value.value * 100)}%`); break; case "number": { parsed.push(round(arg.value.value)); break; } case "parenthesis-block": { parsed.push("("); break; } case "close-parenthesis": parsed.push(")"); break; case "string": case "function": case "ident": case "at-keyword": case "hash": case "id-hash": case "unquoted-url": case "dimension": case "white-space": case "comment": case "colon": case "semicolon": case "comma": case "include-match": case "dash-match": case "prefix-match": case "suffix-match": case "substring-match": case "cdo": case "cdc": case "square-bracket-block": case "curly-bracket-block": case "bad-url": case "bad-string": case "close-square-bracket": case "close-curly-bracket": } } } return parsed; } export function parseTranslateProp( value: Translate, prop: keyof Extract, builder: StylesheetBuilder, ): StyleDescriptor { if (value === "none") { return 0; } return parseLength(value[prop], builder); } export function parseUnresolvedColor( color: UnresolvedColor, builder: StylesheetBuilder, property: string, allowAuto: boolean, ): StyleDescriptor { switch (color.type) { case "rgb": return [ {}, "rgba", [ round(color.r * 255), round(color.g * 255), round(color.b * 255), parseUnparsed(color.alpha, builder, property), ], ]; case "hsl": return [ {}, color.type, [ color.h, color.s, color.l, parseUnparsed(color.alpha, builder, property), ], ]; case "light-dark": { const extraRule = builder.extendRule({ m: [["=", "prefers-color-scheme", "dark"]], }); builder.addUnnamedDescriptor( reduceParseUnparsed(color.dark, builder, property, allowAuto), false, extraRule, ); builder.addExtraRule(extraRule); return reduceParseUnparsed(color.light, builder, property, allowAuto); } default: color satisfies never; } return; } export function allEqual(...params: unknown[]) { return params.every((param, index, array) => { return index === 0 ? true : equal(array[0], param); }); } export function equal(a: unknown, b: unknown) { if (a === b) return true; if (typeof a !== typeof b) return false; if (a === null || b === null) return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!equal(a[i], b[i])) return false; } return true; } if (typeof a === "object" && typeof b === "object") { if (Object.keys(a).length !== Object.keys(b).length) return false; for (const key in a) { if ( !equal( (a as Record)[key], (b as Record)[key], ) ) return false; } return true; } return false; } export function parseTime(time: Time) { return round(time.type === "milliseconds" ? time.value : time.value * 1000); } export function parseSize2DDimensionPercentageDeclaration( declaration: { value: Size2DFor_DimensionPercentageFor_LengthValue }, builder: StylesheetBuilder, ) { return parseSize2DDimensionPercentage(declaration.value, builder); } export function parseSize2DDimensionPercentage( value: Size2DFor_DimensionPercentageFor_LengthValue, builder: StylesheetBuilder, ) { return parseLength(value[0], builder); } export function addTransitionValue( declaration: Extract< Declaration, { property: `transition${string}` | "transition" } >, builder: StylesheetBuilder, ) { switch (declaration.property) { case "transition": { const grouped: Record = {}; for (const animation of declaration.value) { for (const [key, value] of Object.entries(animation)) { grouped[key] ??= []; grouped[key].push(value); } } for (const [property, value] of Object.entries(grouped)) { addTransitionValue( { property: `transition-${kebabCase(property)}`, value, } as Extract< Declaration, { property: `transition${string}` | "transition" } >, builder, ); } break; } case "transition-property": { builder.addDescriptor( declaration.property, declaration.value .map((v) => v.property) .filter( (v) => (v in parsers && !["visibility"].includes(v)) || v === "all" || v === "none", ) .map((v) => toRNProperty(v)), ); return; } case "transition-duration": builder.addDescriptor( declaration.property, declaration.value.map((t) => parseTime(t)), ); return; case "transition-delay": builder.addDescriptor( declaration.property, declaration.value.map((t) => parseTime(t)), ); return; case "transition-timing-function": builder.addDescriptor( declaration.property, parseEasingFunction(declaration.value), ); return; } } export function addAnimationValue( declaration: Extract< Declaration, { property: `animation${string}` | "animation" } >, builder: StylesheetBuilder, ) { switch (declaration.property) { case "animation": { const grouped: Record = {}; for (const animation of declaration.value) { for (const [key, value] of Object.entries(animation)) { grouped[key] ??= []; grouped[key].push(value); } } for (const [property, value] of Object.entries(grouped)) { addAnimationValue( { property: `animation-${kebabCase(property)}`, value, } as Extract< Declaration, { property: `animation${string}` | "animation" } >, builder, ); } break; } case "animation-delay": { builder.addDescriptor( declaration.property, declaration.value.map((t) => parseTime(t)), ); break; } case "animation-direction": { builder.addDescriptor(declaration.property, declaration.value); break; } case "animation-duration": { builder.addDescriptor( declaration.property, declaration.value.map((t) => parseTime(t)), ); break; } case "animation-fill-mode": { builder.addDescriptor(declaration.property, declaration.value); break; } case "animation-iteration-count": { builder.addDescriptor( declaration.property, parseIterationCount(declaration.value), ); break; } case "animation-name": { builder.addDescriptor( declaration.property, declaration.value.map((v) => v.type === "none" ? "none" : ([{}, "animationName", [v.value], 1] as StyleDescriptor), ), ); break; } case "animation-play-state": { builder.addDescriptor(declaration.property, declaration.value); break; } case "animation-timing-function": { builder.addDescriptor( declaration.property, parseEasingFunction(declaration.value), ); break; } } } export function kebabCase(str: string) { return str.replace( /[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? "-" : "") + $.toLowerCase(), ); } function parseBackgroundImage( declaration: DeclarationType<"background-image">, builder: StylesheetBuilder, ) { builder.addDescriptor( "experimental_backgroundImage", declaration.value.flatMap((image): StyleDescriptor[] => { switch (image.type) { case "gradient": { const gradient = parseGradient(image.value, builder); return gradient ? [gradient] : []; } case "none": return ["none"]; } return []; }), ); return; } function parseGradient( gradient: Gradient, builder: StylesheetBuilder, ): StyleDescriptor { switch (gradient.type) { case "linear": { return [ {}, "linear-gradient", [ parseLineDirection(gradient.direction, builder), ...gradient.items.map((item) => parseGradientItem(item, builder)), ], ]; } } return; } function parseLineDirection( lineDirection: LineDirection, builder: StylesheetBuilder, ): StyleDescriptor { switch (lineDirection.type) { case "corner": return `to ${lineDirection.horizontal} ${lineDirection.vertical}`; case "horizontal": case "vertical": return `to ${lineDirection.value}`; case "angle": return parseAngle(lineDirection.value, builder); default: { lineDirection satisfies never; } } return; } function parseGradientItem( item: GradientItemFor_DimensionPercentageFor_LengthValue, builder: StylesheetBuilder, ): StyleDescriptor { switch (item.type) { case "color-stop": { const args: StyleDescriptor[] = [parseColor(item.color, builder)]; if (item.position) { args.push(parseLength(item.position, builder)); } return [{}, "@colorStop", args, 1]; } case "hint": return parseLength(item.value, builder); } } function parseObjectFit( declaration: CustomProperty, builder: StylesheetBuilder, ) { builder.addMapping({ "object-fit": ["contentFit"], }); builder.addDescriptor( "object-fit", parseUnparsed(declaration.value, builder, "object-fit"), ); } function parseObjectPosition( declaration: CustomProperty, builder: StylesheetBuilder, ) { builder.addMapping({ "object-position": ["contentPosition"], }); builder.addDescriptor("object-position", [ {}, "join", [parseUnparsed(declaration.value, builder, "object-position"), " "], ]); } function parseCornerShape( declaration: CustomProperty, builder: StylesheetBuilder, ) { const shape = parseUnparsed(declaration.value, builder, "corner-shape"); if (shape === "round") { builder.addDescriptor("borderCurve", "circular"); } else if (shape === "squircle") { builder.addDescriptor("borderCurve", "continuous"); } } function parseVisibility( declaration: DeclarationType<"visibility">, builder: StylesheetBuilder, ) { if (declaration.value === "visible") { builder.addDescriptor("opacity", 1); } else if (declaration.value === "hidden") { builder.addDescriptor("opacity", 0); } else { builder.addWarning("value", declaration.value); } } function parseOutlineStyle( declaration: DeclarationType<"outline-style">, builder: StylesheetBuilder, ) { const allowed = ["solid", "dotted", "dashed"]; if ( declaration.value.type !== "auto" && allowed.includes(declaration.value.value) ) { builder.addDescriptor("outlineStyle", declaration.value.value); } else { builder.addWarning( "value", declaration.value.type === "auto" ? "auto" : declaration.value.value, ); } } function parseFilter( declaration: DeclarationType<"filter">, builder: StylesheetBuilder, ) { if (declaration.value.type === "none") { return "unset"; } return declaration.value.value .map((value) => { switch (value.type) { case "opacity": case "blur": case "brightness": case "contrast": case "grayscale": case "invert": case "saturate": case "sepia": return { [value.type]: parseLength(value.value, builder), } as unknown as StyleDescriptor; case "hue-rotate": return { [value.type]: parseAngle(value.value, builder), } as unknown as StyleDescriptor; case "drop-shadow": return [ {}, toRNProperty(value.type), [ parseLength(value.value.xOffset, builder), parseLength(value.value.yOffset, builder), parseLength(value.value.blur, builder), parseColor(value.value.color, builder), ], ] as unknown as StyleDescriptor; case "url": return; } }) .filter((value) => value !== undefined); } const namedColors = new Set([ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen", ]);