diff --git a/src/compiler/atRules.ts b/src/compiler/atRules.ts index 6943f672..730aef41 100644 --- a/src/compiler/atRules.ts +++ b/src/compiler/atRules.ts @@ -6,8 +6,8 @@ import type { TokenOrValue, } from "lightningcss"; +import { toRNProperty } from "../utilities"; import type { StyleRuleMapping } from "./compiler.types"; -import { toRNProperty } from "./selectors"; import { splitByDelimiter } from "./split-by-delimiter"; import type { StylesheetBuilder } from "./stylesheet"; diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 9ca2aab8..62777dfb 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -36,7 +36,7 @@ import type { UnresolvedColor, } from "lightningcss"; -import { isStyleFunction } from "../utilities"; +import { isStyleFunction, toRNProperty } from "../utilities"; import { isMappingProperty } from "./atRules"; import type { StyleDescriptor, @@ -44,7 +44,6 @@ import type { StyleRule, } from "./compiler.types"; import { parseEasingFunction, parseIterationCount } from "./keyframes"; -import { toRNProperty } from "./selectors"; import type { StylesheetBuilder } from "./stylesheet"; const CommaSeparator = Symbol("CommaSeparator"); diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts index 6787c0ca..f2a2cd75 100644 --- a/src/compiler/selector-builder.ts +++ b/src/compiler/selector-builder.ts @@ -1,6 +1,6 @@ import type { AttrOperation, Selector, SelectorList } from "lightningcss"; -import { Specificity } from "../utilities"; +import { Specificity, toRNProperty } from "../utilities"; import type { AttributeQuery, AttrSelectorOperator, @@ -597,17 +597,6 @@ function isUniversalSelector([first, second]: Selector) { return first && first.type === "universal" && !second; } -export function toRNProperty(str: T) { - return str - .replace(/^-rn-/, "") - .replace(/-./g, (x) => x[1]?.toUpperCase() ?? "") as CamelCase; -} - -type CamelCase = - S extends `${infer P1}-${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase; - const operatorMap: Record = { "equal": "=", "includes": "~=", diff --git a/src/compiler/selectors.ts b/src/compiler/selectors.ts deleted file mode 100644 index b8623072..00000000 --- a/src/compiler/selectors.ts +++ /dev/null @@ -1,505 +0,0 @@ -/* eslint-disable */ -import type { Selector, SelectorList } from "lightningcss"; - -import { Specificity } from "../utilities"; -import type { - AttributeQuery, - AttrSelectorOperator, - CompilerOptions, - ContainerQuery, - MediaCondition, - PseudoClassesQuery, - SpecificityArray, -} from "./compiler.types"; -import { StylesheetBuilder } from "./stylesheet"; - -export type NormalizeSelector = - | ClassNameSelector - | { - type: "rootVariables" | "universalVariables"; - subtype: "light" | "dark"; - }; - -type ClassNameSelector = { - type: "className"; - specificity: SpecificityArray; - className: string; - mediaQuery?: MediaCondition[]; - containerQuery?: ContainerQuery[]; - pseudoClassesQuery?: PseudoClassesQuery; - attributeQuery?: AttributeQuery[]; -}; - -/** - * Turns a CSS selector into a `react-native-css` selector. - */ -export function getSelectors( - selectorList: SelectorList, - isDarkMode: boolean, - builder: StylesheetBuilder, - selectors: NormalizeSelector[] = [], -) { - for (let cssSelector of selectorList) { - // Ignore `:is()` & `:where()`, and just process its selectors - if (isIsPseudoClass(cssSelector) || isWherePseudoClass(cssSelector)) { - getSelectors(cssSelector[0].selectors, isDarkMode, builder, selectors); - } else if ( - // Matches: :root {} - isRootVariableSelector(cssSelector) - ) { - // Matches: @media(prefers-dark-mode) { :root {} } - if (isDarkMode) { - selectors.push({ - type: "rootVariables", - subtype: "dark", - }); - } else { - selectors.push({ - type: "rootVariables", - subtype: "light", - }); - } - } else if ( - // Matches: .dark:root {} || :root[class~="dark"] - isRootDarkVariableSelector(cssSelector) - ) { - selectors.push({ - type: "rootVariables", - subtype: "dark", - }); - } else if ( - // Matches @media(prefers-dark-mode) { * {} } - isDarkMode && - isDefaultVariableSelector(cssSelector) - ) { - selectors.push({ - type: "universalVariables", - subtype: "dark", - }); - } else if ( - // Matches: * {} - isDefaultVariableSelector(cssSelector) - ) { - selectors.push({ - type: "universalVariables", - subtype: "light", - }); - // } else if ( - // // Matches: .dark * {} - // isDarkUniversalSelector(cssSelector) - // ) { - // selectors.push({ - // type: "universalVariables", - // subtype: "dark", - // }); - // } else if ( - // // Matches: .dark {} - // isDarkClassLegacySelector(cssSelector, collection) - // ) { - // const [_, __, third, ...rest] = cssSelector; - - // getSelectors(extractedStyle, [[third!, ...rest]], collection, selectors, [ - // ["=", "prefers-color-scheme", "dark"], - // ]); - // } else if ( - // // Matches: :is(.dark *) {} - // isDarkClassSelector(cssSelector, collection) - // ) { - // const [first] = cssSelector; - - // getSelectors(extractedStyle, [[first!]], collection, selectors, [ - // ["=", "prefers-color-scheme", "dark"], - // ]); - } else { - const selector = classNameSelector(cssSelector, builder.getOptions()); - - if (selector === null) { - continue; - } - - selectors.push(selector); - } - } - - return selectors; -} - -function classNameSelector( - selector: Selector, - options: CompilerOptions, -): ClassNameSelector | null { - let primaryClassName: string | undefined; - const specificity: SpecificityArray = []; - let mediaQuery: MediaCondition[] | undefined; - let containerQuery: ContainerQuery[] | undefined; - let attributeQuery: AttributeQuery[] | undefined; - let pseudoClassesQuery: PseudoClassesQuery | undefined; - - let currentContainerQuery: ContainerQuery | undefined; - - let isInClassBlock = false; - let newBlock = false; - - function getAttributeQuery() { - if (newBlock) { - currentContainerQuery ??= {}; - } - - if (currentContainerQuery) { - if (!currentContainerQuery.a) { - currentContainerQuery.a = []; - } - return currentContainerQuery.a; - } else { - attributeQuery ??= []; - return attributeQuery; - } - } - - function getPseudoClassesQuery() { - if (newBlock) { - currentContainerQuery ??= {}; - } - - if (currentContainerQuery) { - if (!currentContainerQuery.p) { - currentContainerQuery.p = {}; - } - return currentContainerQuery.p; - } else { - pseudoClassesQuery ??= {}; - return pseudoClassesQuery; - } - } - - /* - * Loop over each token in reverse order. - */ - for (const component of selector.reverse()) { - switch (component.type) { - case "universal": - case "namespace": - case "id": - case "pseudo-element": - // We don't support these selectors at all - return null; - case "nesting": - continue; - case "class": { - if (!primaryClassName) { - primaryClassName = component.name; - } else if (isInClassBlock) { - getAttributeQuery().unshift(["a", "className", "*=", component.name]); - } else if (component.name !== options.selectorPrefix) { - if (currentContainerQuery?.n) { - getAttributeQuery().unshift([ - "a", - "className", - "*=", - component.name, - ]); - } else { - currentContainerQuery ??= {}; - currentContainerQuery.n = component.name; - - containerQuery ??= []; - containerQuery.unshift(currentContainerQuery); - } - } - - isInClassBlock = true; - newBlock = false; - - specificity[Specificity.ClassName] = - (specificity[Specificity.ClassName] ?? 0) + 1; - - break; - } - case "pseudo-class": { - specificity[Specificity.ClassName] = - (specificity[Specificity.ClassName] ?? 0) + 1; - - // if (component.kind === "is") { - // if (isDarkUniversalSelector(component.selectors[0]!, collection)) { - // const lastGroup = acc.classNames[acc.classNames.length - 1]!; - // lastGroup[1] ??= {}; - // lastGroup[1].m ??= []; - // lastGroup[1].m.push(["=", "prefers-color-scheme", "dark"]); - // break; - // } - // } - - switch (component.kind) { - case "hover": { - getPseudoClassesQuery().h = 1; - break; - } - case "active": { - getPseudoClassesQuery().a = 1; - break; - } - case "focus": { - getPseudoClassesQuery().f = 1; - break; - } - case "disabled": { - getAttributeQuery().push(["a", "disabled"]); - break; - } - case "empty": { - getAttributeQuery().push(["a", "children", "!"]); - break; - } - default: { - // We don't support other pseudo-classes - return null; - } - } - break; - } - case "attribute": { - // We don't support attribute selectors as standalone selectors - // Except for a top level [dir] selector - // if (classNames.length === 0) { - // if ( - // component.name === "dir" && - // component.operation?.operator === "equal" - // ) { - // mediaQuery ??= []; - // mediaQuery.push(["!!", component.operation.value]); - // break; - // } - - // // Ignore any other attributes that are not part of a class selector - // return null; - // } - - // Turn attribute selectors into AttributeConditions - specificity[Specificity.ClassName] = - (specificity[Specificity.ClassName] ?? 0) + 1; - - // [data-*] are turned into `dataSet` queries - // Everything else is turned into `attribute` queries - const attributeQuery: AttributeQuery = component.name.startsWith( - "data-", - ) - ? ["d", toRNProperty(component.name.replace("data-", ""))] - : ["a", toRNProperty(component.name)]; - - if (component.operation) { - let operator: AttrSelectorOperator | undefined; - - switch (component.operation.operator) { - case "equal": - operator = "="; - break; - case "includes": - operator = "~="; - break; - case "dash-match": - operator = "|="; - break; - case "prefix": - operator = "^="; - break; - case "substring": - operator = "*="; - break; - case "suffix": - operator = "$="; - break; - default: - component.operation.operator satisfies never; - break; - } - - if (operator) { - attributeQuery.push(operator, component.operation.value); - } - } - - getAttributeQuery().push(attributeQuery); - break; - } - case "type": { - /* - * We only support type selectors as part of the selector prefix - * For example: `html .my-class` - * - * NOTE: We ignore specificity for this - */ - if (component.name === options.selectorPrefix) { - break; - } - return null; - } - case "combinator": { - // We only support the descendant combinator - if (component.value === "descendant") { - isInClassBlock = false; - newBlock = true; - currentContainerQuery = undefined; - break; - } - - return null; - } - } - } - - if (!primaryClassName) { - // No class name found, return null - return null; - } - - return { - type: "className", - specificity: specificity, - className: primaryClassName, - mediaQuery, - containerQuery, - pseudoClassesQuery, - attributeQuery, - }; -} - -function isIsPseudoClass( - selector: Selector, -): selector is [{ type: "pseudo-class"; kind: "is"; selectors: Selector[] }] { - return ( - selector.length === 1 && - selector[0]?.type === "pseudo-class" && - selector[0].kind === "is" - ); -} - -function isWherePseudoClass( - selector: Selector, -): selector is [ - { type: "pseudo-class"; kind: "where"; selectors: Selector[] }, -] { - return ( - selector.length === 1 && - selector[0]?.type === "pseudo-class" && - selector[0].kind === "where" - ); -} - -// function isDarkModeMediaQuery(query?: MediaCondition): boolean { -// if (!query) return false; - -// return ( -// query[0] === "=" && -// query[1] === "prefers-color-scheme" && -// query[2] === "dark" -// ); -// } - -// Matches: :is(.dark *) -// function isDarkClassSelector( -// [first, second, third]: Selector, -// collection: CompilerCollection, -// ) { -// if (!collection.darkMode) { -// return false; -// } - -// return ( -// first && -// second && -// !third && -// first.type === "class" && -// second.type === "pseudo-class" && -// second.kind === "is" && -// second.selectors.length === 1 && -// second.selectors[0]?.length === 3 && -// second.selectors[0][0]?.type === "class" && -// second.selectors[0][0].name === collection.darkMode && -// second.selectors[0][1]?.type === "combinator" && -// second.selectors[0][1].value === "descendant" && -// second.selectors[0][2]?.type === "universal" -// ); -// } - -// Matches: .dark {} -// function isDarkClassLegacySelector( -// [first, second, third]: Selector, -// collection: CompilerCollection, -// ) { -// if (!collection.darkMode) { -// return false; -// } - -// return ( -// first && -// second && -// third && -// first.type === "class" && -// first.name === collection.darkMode && -// second.type === "combinator" && -// second.value === "descendant" && -// third.type === "class" -// ); -// } - -// Matches: :root {} -function isRootVariableSelector([first, second]: Selector) { - return ( - first && !second && first.type === "pseudo-class" && first.kind === "root" - ); -} - -// Matches: * {} -function isDefaultVariableSelector([first, second]: Selector) { - return first && !second && first.type === "universal"; -} - -// Matches: .dark:root {} -function isRootDarkVariableSelector([first, second]: Selector) { - return ( - first && - second && - // .dark:root {} - ((first.type === "class" && - second.type === "pseudo-class" && - second.kind === "root") || - // :root[class~=dark] {} - (first.type === "pseudo-class" && - first.kind === "root" && - second.type === "attribute" && - second.name === "class" && - second.operation && - ["includes", "equal"].includes(second.operation.operator))) - ); -} - -// Matches: .dark * {} -// function isDarkUniversalSelector( -// [first, second, third]: Selector, -// collection: CompilerCollection, -// ) { -// if (!collection.darkMode) { -// return false; -// } -// return ( -// first && -// second && -// third && -// first.type === "class" && -// first.name === collection.darkMode && -// second.type === "combinator" && -// second.value === "descendant" && -// third.type === "universal" -// ); -// } - -export function toRNProperty(str: T) { - return str - .replace(/^-rn-/, "") - .replace(/-./g, (x) => x[1]!.toUpperCase()) as CamelCase; -} - -type CamelCase = - S extends `${infer P1}-${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase; diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts index a77fdf89..ffb4c581 100644 --- a/src/compiler/stylesheet.ts +++ b/src/compiler/stylesheet.ts @@ -4,6 +4,7 @@ import { isStyleDescriptorArray, Specificity, specificityCompareFn, + toRNProperty, } from "../utilities"; import type { AnimationKeyframes, @@ -25,7 +26,7 @@ import { modifyRuleForPlaceholder, modifyRuleForSelection, } from "./pseudo-elements"; -import { getClassNameSelectors, toRNProperty } from "./selector-builder"; +import { getClassNameSelectors } from "./selector-builder"; type BuilderMode = "style" | "media" | "container" | "keyframes"; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 0c95da63..4470e985 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,4 @@ export * from "./specificity"; export * from "./style-descriptor"; export * from "./dot-notation.types"; +export * from "./property-names"; diff --git a/src/utilities/property-names.ts b/src/utilities/property-names.ts new file mode 100644 index 00000000..ee8fabe2 --- /dev/null +++ b/src/utilities/property-names.ts @@ -0,0 +1,10 @@ +type CamelCase = + S extends `${infer P1}-${infer P2}${infer P3}` + ? `${Lowercase}${Uppercase}${CamelCase}` + : Lowercase; + +export function toRNProperty(str: T) { + return str + .replace(/^-rn-/, "") + .replace(/-./g, (x) => x[1]?.toUpperCase() ?? "") as CamelCase; +}