diff --git a/src/compiler/__tests__/compiler.test.tsx b/src/compiler/__tests__/compiler.test.tsx index 4f7cacf..c790998 100644 --- a/src/compiler/__tests__/compiler.test.tsx +++ b/src/compiler/__tests__/compiler.test.tsx @@ -246,3 +246,37 @@ test("breaks apart comma separated variables", () => { vr: [["test", [["blue", "green"]]]], }); }); + +test("light-dark()", () => { + const compiled = compile(` +.my-class { + background-color: light-dark(#333b3c, #efefec); +}`); + + expect(compiled).toStrictEqual({ + s: [ + [ + "my-class", + [ + { + d: [ + { + backgroundColor: "#333b3c", + }, + ], + s: [1, 1], + }, + { + d: [ + { + backgroundColor: "#efefec", + }, + ], + m: [["=", "prefers-color-scheme", "dark"]], + s: [1, 1], + }, + ], + ], + ], + }); +}); diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index a65a924..3793502 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -35,7 +35,11 @@ import type { UnresolvedColor, } from "lightningcss"; -import type { StyleDescriptor, StyleFunction } from "./compiler.types"; +import type { + StyleDescriptor, + StyleFunction, + StyleRule, +} from "./compiler.types"; import { parseEasingFunction, parseIterationCount } from "./keyframes"; import { toRNProperty } from "./selectors"; import type { StylesheetBuilder } from "./stylesheet"; @@ -47,9 +51,10 @@ type DeclarationType

= Extract< { property: P } >; -type Parser = ( +type Parser = ( declaration: Extract, builder: StylesheetBuilder, + propertyName: string, // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => StyleDescriptor | void; @@ -225,8 +230,8 @@ const parsers: { }; // This is missing LightningCSS types -(parsers as Record>)["pointer-events"] = - parsePointerEvents as Parser; +(parsers as Record)["pointer-events"] = + parsePointerEvents as Parser; const validProperties = new Set(Object.keys(parsers)); @@ -250,34 +255,30 @@ export function parseDeclaration( if (declaration.property === "unparsed") { parseDeclarationUnparsed(declaration, builder); - return; } else if (declaration.property === "custom") { parseDeclarationCustom(declaration, builder); - return; + } else { + parseWithParser(declaration, builder); } +} +function parseWithParser(declaration: Declaration, builder: StylesheetBuilder) { if (declaration.property in parsers) { - const property = - propertyRename[declaration.property] ?? declaration.property; + const parser = parsers[declaration.property] as Parser; - const parser = parsers[ - declaration.property as keyof typeof parsers - ] as unknown as ( - b: typeof declaration, - builder: StylesheetBuilder, - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - ) => StyleDescriptor | void; + builder.descriptorProperty = declaration.property; - const value = parser(declaration, builder); + const value = parser(declaration, builder, declaration.property); if (value !== undefined) { - builder.addDescriptor(property, value); + builder.addDescriptor( + propertyRename[declaration.property] ?? declaration.property, + value, + ); } } else { - builder.addWarning("property", (declaration as Declaration).property); + builder.addWarning("property", declaration.property); } - - return; } function parseInsetBlock( @@ -803,7 +804,7 @@ export function parseDeclarationUnparsed( /** * React Native doesn't support all the logical properties */ - const rename = propertyRename[declaration.value.propertyId.property]; + const rename = propertyRename[property]; if (rename) { property = rename; } @@ -811,6 +812,8 @@ export function parseDeclarationUnparsed( /** * Unparsed shorthand properties need to be parsed at runtime */ + builder.descriptorProperty = property; + if (unparsedRuntimeParsing.has(property)) { const args = parseUnparsed(declaration.value.value, builder); @@ -1369,9 +1372,20 @@ export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) { switch (cssColor.type) { case "currentcolor": return [{}, "var", "__rn-css-current-color"] as const; - case "light-dark": - // TODO: Handle light-dark colors - return; + 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", @@ -2451,8 +2465,18 @@ export function parseUnresolvedColor( color.type, [color.h, color.s, color.l, parseUnparsed(color.alpha, builder)], ]; - case "light-dark": - return undefined; + case "light-dark": { + const extraRule = builder.extendRule({ + m: [["=", "prefers-color-scheme", "dark"]], + }); + builder.addUnnamedDescriptor( + reduceParseUnparsed(color.dark, builder), + false, + extraRule, + ); + builder.addExtraRule(extraRule); + return reduceParseUnparsed(color.light, builder); + } default: color satisfies never; } diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts index d180c42..42c3e91 100644 --- a/src/compiler/stylesheet.ts +++ b/src/compiler/stylesheet.ts @@ -22,10 +22,16 @@ import { toRNProperty, type NormalizeSelector } from "./selectors"; type BuilderMode = "style" | "media" | "container" | "keyframes"; +const staticDeclarations = new WeakMap< + WeakKey, + Record +>(); + +const extraRules = new WeakMap[]>(); + export class StylesheetBuilder { animationFrames?: AnimationKeyframes_V2[]; animationDeclarations: StyleDeclaration[] = []; - staticDeclarations: Record | undefined; stylesheet: ReactNativeCssStyleSheet = {}; @@ -37,11 +43,12 @@ export class StylesheetBuilder { constructor( private options: CompilerOptions, - private mode: BuilderMode = "style", + public mode: BuilderMode = "style", private ruleTemplate: StyleRule = { s: [], }, private mapping: StyleRuleMapping = {}, + public descriptorProperty?: string, private shared: { ruleSets: Record; rootVariables?: VariableRecord; @@ -52,19 +59,20 @@ export class StylesheetBuilder { } = { ruleSets: {}, rem: 14, ruleOrder: 0 }, ) {} - fork(mode: BuilderMode) { + fork(mode = this.mode, rule?: Partial): StylesheetBuilder { this.shared.ruleOrder++; return new StylesheetBuilder( this.options, mode, - this.cloneRule(), + this.cloneRule(rule ? { ...this.rule, ...rule } : undefined), { ...this.mapping }, + this.descriptorProperty, this.shared, ); } cloneRule({ ...rule } = this.rule): StyleRule { - rule.s = [...this.rule.s]; + rule.s = [...rule.s]; rule.aq &&= [...rule.aq]; rule.c &&= [...rule.c]; rule.cq &&= [...rule.cq]; @@ -76,6 +84,25 @@ export class StylesheetBuilder { return rule; } + private createRuleFromPartial(rule: StyleRule, partial: Partial) { + rule = this.cloneRule(rule); + + if (partial.m) { + rule.m ??= []; + rule.m.push(...partial.m); + } + + if (partial.d) { + rule.d = partial.d; + } + + return rule; + } + + extendRule(rule: Partial) { + return this.cloneRule({ ...this.rule, ...rule }); + } + getOptions(): CompilerOptions { return this.options; } @@ -130,9 +157,8 @@ export class StylesheetBuilder { // TODO } - newRule(mapping: StyleRuleMapping, { important = false } = {}) { + newRule(mapping = this.mapping, { important = false } = {}) { this.mapping = mapping; - this.staticDeclarations = undefined; this.rule = this.cloneRule(this.ruleTemplate); this.rule.s[Specificity.Order] = this.shared.ruleOrder; if (important) { @@ -140,7 +166,16 @@ export class StylesheetBuilder { } } - addRuleToRuleSet(name: string, rule = this.rule) { + addExtraRule(rule: Partial) { + let extraRuleArray = extraRules.get(this.rule); + if (!extraRuleArray) { + extraRuleArray = []; + extraRules.set(this.rule, extraRuleArray); + } + extraRuleArray.push(rule); + } + + private addRuleToRuleSet(name: string, rule = this.rule) { if (this.shared.ruleSets[name]) { this.shared.ruleSets[name].push(rule); } else { @@ -163,10 +198,23 @@ export class StylesheetBuilder { } } + addUnnamedDescriptor( + value: StyleDescriptor, + forceTuple?: boolean, + rule = this.rule, + ) { + if (this.descriptorProperty === undefined) { + return; + } + + this.addDescriptor(this.descriptorProperty, value, forceTuple, rule); + } + addDescriptor( property: string, value: StyleDescriptor, forceTuple?: boolean, + rule = this.rule, ) { if (value === undefined) { return; @@ -190,34 +238,34 @@ export class StylesheetBuilder { return; } - this.rule.v ??= []; - this.rule.v.push([property.slice(2), value]); + rule.v ??= []; + rule.v.push([property.slice(2), value]); } else if (isStyleFunction(value)) { const [delayed, usesVariables] = postProcessStyleFunction(value); - this.rule.d ??= []; + rule.d ??= []; if (value[1] === "@animation") { - this.rule.a ??= true; + rule.a ??= true; } if (usesVariables) { - this.rule.dv = 1; + rule.dv = 1; } this.pushDescriptor( property, value, - this.rule.d, + rule.d, forceTuple, delayed || usesVariables, ); } else { if (property.startsWith("animation-")) { - this.rule.a ??= true; + rule.a ??= true; } - this.rule.d ??= []; - this.pushDescriptor(property, value, this.rule.d); + rule.d ??= []; + this.pushDescriptor(property, value, rule.d); } } @@ -269,11 +317,13 @@ export class StylesheetBuilder { } else if (Array.isArray(value) && value.some(isStyleFunction)) { declarations.push([value, propPath]); } else { - if (!this.staticDeclarations) { - this.staticDeclarations = {}; - declarations.push(this.staticDeclarations); + let staticDeclarationRecord = staticDeclarations.get(declarations); + if (!staticDeclarationRecord) { + staticDeclarationRecord = {}; + staticDeclarations.set(declarations, staticDeclarationRecord); + declarations.push(staticDeclarationRecord); } - this.staticDeclarations[propPath] = value; + staticDeclarationRecord[propPath] = value; } } @@ -339,6 +389,16 @@ export class StylesheetBuilder { } this.addRuleToRuleSet(className, rule); + + const extraRulesArray = extraRules.get(this.rule); + if (extraRulesArray) { + for (const extraRule of extraRulesArray) { + this.addRuleToRuleSet( + className, + this.createRuleFromPartial(rule, extraRule), + ); + } + } } else { // These can only have variable declarations if (!this.rule.v) { @@ -379,7 +439,6 @@ export class StylesheetBuilder { } this.animationDeclarations = []; - this.staticDeclarations = undefined; this.animationFrames.push([progress, this.animationDeclarations]); } }