diff --git a/src/compiler/__tests__/selectors.test.tsx b/src/compiler/__tests__/selectors.test.tsx new file mode 100644 index 00000000..a3fac553 --- /dev/null +++ b/src/compiler/__tests__/selectors.test.tsx @@ -0,0 +1,86 @@ +import { getClassNameSelectors } from "../selector-builder"; + +test("empty", () => { + expect(getClassNameSelectors([])).toStrictEqual([]); +}); + +test(".my-class { &:is(:where(.my-parent, .my-second-parent):hover *) {} }", () => { + const result = getClassNameSelectors([ + [ + { + type: "class", + name: "my-class", + }, + { + type: "nesting", + }, + { + type: "pseudo-class", + kind: "is", + selectors: [ + [ + { + type: "pseudo-class", + kind: "where", + selectors: [ + [ + { + type: "class", + name: "my-parent", + }, + ], + + [ + { + type: "class", + name: "my-second-parent", + }, + ], + ], + }, + { + type: "pseudo-class", + kind: "hover", + }, + { + type: "combinator", + value: "descendant", + }, + { + type: "universal", + }, + ], + ], + }, + ], + ]); + + expect(result).toStrictEqual([ + { + className: "my-class", + containerQuery: [ + { + n: "my-parent", + p: { + h: 1, + }, + }, + ], + specificity: [0, 2], + type: "className", + }, + { + className: "my-class", + containerQuery: [ + { + n: "my-second-parent", + p: { + h: 1, + }, + }, + ], + specificity: [0, 2], + type: "className", + }, + ]); +}); diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 2fa2ffe6..24dfea9c 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -19,7 +19,6 @@ import { parseContainerCondition } from "./container-query"; import { parseDeclaration } from "./declarations"; import { extractKeyFrames } from "./keyframes"; import { parseMediaQuery } from "./media-query"; -import { getSelectors } from "./selectors"; import { StylesheetBuilder } from "./stylesheet"; const defaultLogger = debug("react-native-css:compiler"); @@ -165,14 +164,9 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { const declarationBlock = value.declarations; const mapping = parsePropAtRule(value.rules); - const selectors = getSelectors( - value.selectors, - false, - builder.getOptions(), - ); // If the rule is a style declaration, extract it with the `getExtractedStyle` function and store it in the `declarations` map - builder = builder.fork("style", selectors); + builder = builder.fork("style", value.selectors); if (declarationBlock) { if (declarationBlock.declarations) { diff --git a/src/compiler/inheritance.test.ts b/src/compiler/inheritance.test.ts index 021787be..587c39c7 100644 --- a/src/compiler/inheritance.test.ts +++ b/src/compiler/inheritance.test.ts @@ -94,8 +94,9 @@ test("tiers with multiple classes", () => { "three", [ { - c: ["three"], + c: ["three.two"], s: [0], + aq: [["a", "className", "*=", "two"]], }, ], ], @@ -106,8 +107,7 @@ test("tiers with multiple classes", () => { cq: [ { n: "one" }, { - a: [["a", "className", "*=", "two"]], - n: "three", + n: "three.two", }, ], d: [{ color: "#f00" }], diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts new file mode 100644 index 00000000..35c77ed6 --- /dev/null +++ b/src/compiler/selector-builder.ts @@ -0,0 +1,531 @@ +import type { Selector, SelectorList } from "lightningcss"; + +import { Specificity } from "../runtime/utils"; +import type { + AttributeQuery, + AttrSelectorOperator, + CompilerOptions, + ContainerQuery, + MediaCondition, + PseudoClassesQuery, + SpecificityArray, +} from "./compiler.types"; + +interface ReactNativeClassNameSelector { + type: "className"; + specificity: SpecificityArray; + className: string; + mediaQuery?: MediaCondition[]; + containerQuery?: ContainerQuery[]; + pseudoClassesQuery?: PseudoClassesQuery; + attributeQuery?: AttributeQuery[]; +} + +interface ReactNativeGlobalSelector { + type: "rootVariables" | "universalVariables"; +} + +type PartialSelector = Partial & { + type: "className"; + specificity: SpecificityArray; +}; + +const containerQueryMap = new WeakMap(); +const attributeQueryMap = new WeakMap(); +const pseudoClassesQueryMap = new WeakMap(); + +type ContainerQueryWithSpecificity = ContainerQuery & { + specificity: SpecificityArray; +}; + +export function getClassNameSelectors( + selectors: SelectorList, + options: CompilerOptions = {}, + specificity: SpecificityArray = [0], + root: PartialSelector = { + type: "className", + specificity: [], + }, +) { + if (!selectors.length) { + return []; + } + + return selectors.flatMap( + (selector): (ReactNativeGlobalSelector | PartialSelector)[] => { + if (isRootVariableSelector(selector)) { + return [{ type: "rootVariables" }]; + } else { + return ( + parseComponents( + selector.reverse(), + options, + root, + root, + specificity, + ) ?? [] + ); + } + }, + ); +} + +function parseComponents( + [component, ...rest]: Selector, + options: CompilerOptions, + root: PartialSelector, + ref: PartialSelector | ContainerQuery, + specificity: SpecificityArray, +): PartialSelector[] | null { + if (!component || Array.isArray(component.type)) { + // Merge the specificity with the root specificity + for (let i = 0; i < specificity.length; i++) { + const value = specificity[i]; + if (value !== undefined) { + root.specificity[i] = (root.specificity[i] ?? 0) + value; + } + } + + // Return the root + return [root]; + } + + switch (component.type) { + case "id": // #id + case "namespace": // @namespace + case "universal": // * - universal selector + return null; + case "type": { + // div, span + if ( + component.name === options.selectorPrefix || + component.name === "html" + ) { + return parseComponents(rest, options, root, ref, specificity); + } else { + return null; + } + } + case "nesting": + // & + // The SelectorList should be flattened, so we can skip these + return parseComponents(rest, options, root, ref, specificity); + case "combinator": { + // We only support the descendant combinator + if (component.value === "descendant") { + // Switch to now parsing a container query + ref = {}; + return parseComponents(rest, options, root, ref, specificity); + } + + return []; + } + case "pseudo-element": { + // TODO: Support ::selection, ::placeholder, etc + return []; + } + case "pseudo-class": { + switch (component.kind) { + case "hover": { + getPseudoClassesQuery(ref).h = 1; + specificity[Specificity.PseudoClass] = + (specificity[Specificity.PseudoClass] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "active": { + getPseudoClassesQuery(ref).a = 1; + specificity[Specificity.PseudoClass] = + (specificity[Specificity.PseudoClass] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "focus": { + getPseudoClassesQuery(ref).f = 1; + specificity[Specificity.PseudoClass] = + (specificity[Specificity.PseudoClass] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "disabled": { + getAttributeQuery(ref).push(["a", "disabled"]); + specificity[Specificity.PseudoClass] = + (specificity[Specificity.PseudoClass] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "empty": { + getAttributeQuery(ref).push(["a", "children", "!"]); + specificity[Specificity.PseudoClass] = + (specificity[Specificity.PseudoClass] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "where": + case "is": { + // Now get the selectors inside the `is` or `where` pseudo-class + const isWhereContainerQueries = component.selectors.flatMap( + (selector) => { + return parseIsWhereComponents(component.kind, selector) ?? []; + }, + ); + + // Remember we're looping in reverse order, + // So `rest` contains the selectors BEFORE this one + const parents = parseComponents( + rest, + options, + root, + ref, + specificity, + ); + + if (!parents) { + return null; + } + + // Each parent selector should be combined with each pseudo-class selector + return parents.flatMap((parent) => { + const originalParent = { ...parent }; + + return isWhereContainerQueries.map(({ specificity, ...query }) => { + parent = { ...originalParent }; + parent.specificity = [...originalParent.specificity]; + parent.containerQuery = [ + ...(originalParent.containerQuery ?? []), + ]; + + if (component.kind === "is") { + for (let i = 0; i < specificity.length; i++) { + const value = specificity[i]; + if (value !== undefined) { + parent.specificity[i] = + (parent.specificity[i] ?? 0) + value; + } + } + } + + parent.containerQuery.push(query); + + return parent; + }); + }); + } + default: { + return []; + } + } + } + case "attribute": { + // specificity[Specificity.ClassName] = + // (specificity[Specificity.ClassName] ?? 0) + 1; + const attributeQuery: AttributeQuery = component.name.startsWith("data-") + ? // [data-*] are turned into `dataSet` queries + ["d", toRNProperty(component.name.replace("data-", ""))] + : // Everything else is turned into `attribute` queries + ["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) { + // Append the operator onto the attribute query + attributeQuery.push(operator, component.operation.value); + } + } + getAttributeQuery(ref).push(attributeQuery); + specificity[Specificity.ClassName] = + (specificity[Specificity.ClassName] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + case "class": { + if (component.name === options.selectorPrefix) { + // Skip this one + return parseComponents(rest, options, root, ref, specificity); + } else if (!isContainerQuery(ref) && !ref.className) { + ref.className = component.name; + specificity[Specificity.ClassName] = + (specificity[Specificity.ClassName] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } else if (!isContainerQuery(ref)) { + // Only the first className is used, the rest are attribute queries + getAttributeQuery(ref).unshift([ + "a", + "className", + "*=", + component.name, + ]); + } else { + let containerQueries = containerQueryMap.get(root); + if (!containerQueries) { + containerQueries = []; + root.containerQuery = containerQueries; + containerQueryMap.set(root, containerQueries); + } + if (!ref.n) { + containerQueries.unshift(ref); + } + + ref.n = ref.n ? `${ref.n}.${component.name}` : component.name; + } + + specificity[Specificity.ClassName] = + (specificity[Specificity.ClassName] ?? 0) + 1; + return parseComponents(rest, options, root, ref, specificity); + } + } +} + +function parseIsWhereComponents( + type: "is" | "where", + selector: Selector, + index = 0, + queries?: ContainerQueryWithSpecificity[], +): ContainerQueryWithSpecificity[] | null { + const component = selector[index]; + + if (!component || Array.isArray(component.type)) { + return queries ?? []; + } + + switch (component.type) { + // These are not allowed in `is()` or `where()` + case "id": // #id + case "namespace": // @namespace + case "type": // div, span + case "nesting": // & + case "pseudo-element": // ::selection, ::placeholder, etc + return null; + case "combinator": { + // We only support the descendant combinator + if (component.value === "descendant") { + // Each "block" is a new container query + const children = parseIsWhereComponents(type, selector, index + 1); + return children && queries ? [...queries, ...children] : children; + } + return null; + } + case "universal": { + // * - universal selector + if (index !== selector.length - 1) { + // We only accept it in the last position + return null; + } + + // This was the only component, so we return the ref + if (selector.length === 1) { + return queries ?? [{ specificity: [] }]; + } + + const previous = selector[index - 1]; + + // If the previous component is not a descendant combinator, + if ( + !previous || + previous.type !== "combinator" || + previous.value !== "descendant" + ) { + return null; + } + + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "pseudo-class": { + // const specificity = ref.specificity; + + // specificity[Specificity.ClassName] = + // (specificity[Specificity.ClassName] ?? 0) + 1; + switch (component.kind) { + case "hover": { + queries ??= [{ specificity: [] }]; + queries.forEach((query) => { + getPseudoClassesQuery(query).h = 1; + }); + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "active": { + queries ??= [{ specificity: [] }]; + queries.forEach((query) => { + getPseudoClassesQuery(query).a = 1; + }); + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "focus": { + queries ??= [{ specificity: [] }]; + queries.forEach((query) => { + getPseudoClassesQuery(query).f = 1; + }); + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "disabled": { + queries ??= [{ specificity: [] }]; + queries.forEach((query) => { + getAttributeQuery(query).push(["a", "disabled"]); + }); + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "empty": { + queries ??= [{ specificity: [] }]; + queries.forEach((query) => { + getAttributeQuery(query).push(["a", "children", "!"]); + }); + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "where": + case "is": { + // :is() and :where() need to be at the start of the selector, + if (index !== 0) { + return null; + } + + // Now get the selectors inside the `is` or `where` pseudo-class + queries = component.selectors.flatMap((selector) => { + return parseIsWhereComponents(type, selector, 0, queries) ?? []; + }); + + return parseIsWhereComponents(type, selector, index + 1, queries); + } + default: { + return null; + } + } + } + case "attribute": { + if (type !== "where") { + // specificity[Specificity.ClassName] = + // (specificity[Specificity.ClassName] ?? 0) + 1; + } + const attributeQuery: AttributeQuery = component.name.startsWith("data-") + ? // [data-*] are turned into `dataSet` queries + ["d", toRNProperty(component.name.replace("data-", ""))] + : // Everything else is turned into `attribute` queries + ["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) { + // Append the operator onto the attribute query + attributeQuery.push(operator, component.operation.value); + } + } + queries ??= [{ specificity: [] }]; + for (const query of queries) { + if (type === "is") { + query.specificity[Specificity.ClassName] = + (query.specificity[Specificity.ClassName] ?? 0) + 1; + } + getAttributeQuery(query).push(attributeQuery); + } + return parseIsWhereComponents(type, selector, index + 1, queries); + } + case "class": { + // In `is` and `where` selectors, the ref will always be a container query + queries ??= [{ specificity: [] }]; + for (const query of queries) { + if (type === "is") { + query.specificity[Specificity.ClassName] = + (query.specificity[Specificity.ClassName] ?? 0) + 1; + } + + query.n = query.n ? `${query.n}.${component.name}` : component.name; + } + + return parseIsWhereComponents(type, selector, index + 1, queries); + } + } +} + +function isContainerQuery( + value: PartialSelector | ContainerQuery, +): value is ContainerQuery { + return !("type" in value); +} + +function getPseudoClassesQuery(key: PartialSelector | ContainerQuery) { + let pseudoClassesQuery = pseudoClassesQueryMap.get(key); + if (!pseudoClassesQuery) { + if ("type" in key) { + pseudoClassesQuery = {}; + key.pseudoClassesQuery = pseudoClassesQuery; + } else { + key.p ??= {}; + pseudoClassesQuery = key.p; + } + pseudoClassesQueryMap.set(key, pseudoClassesQuery); + } + + return pseudoClassesQuery; +} + +function getAttributeQuery( + key: PartialSelector | ContainerQuery, +): AttributeQuery[] { + let attributeQuery = attributeQueryMap.get(key); + if (!attributeQuery) { + if ("type" in key) { + attributeQuery = []; + key.attributeQuery = attributeQuery; + } else { + key.a ??= []; + attributeQuery = key.a; + } + attributeQueryMap.set(key, attributeQuery); + } + + return attributeQuery; +} + +function isRootVariableSelector([first, second]: Selector) { + return ( + first && !second && first.type === "pseudo-class" && first.kind === "root" + ); +} + +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/selectors.ts b/src/compiler/selectors.ts index 0b584990..8d69a654 100644 --- a/src/compiler/selectors.ts +++ b/src/compiler/selectors.ts @@ -11,6 +11,7 @@ import type { PseudoClassesQuery, SpecificityArray, } from "./compiler.types"; +import { StylesheetBuilder } from "./stylesheet"; export type NormalizeSelector = | ClassNameSelector @@ -35,13 +36,13 @@ type ClassNameSelector = { export function getSelectors( selectorList: SelectorList, isDarkMode: boolean, - options: CompilerOptions, + builder: StylesheetBuilder, selectors: NormalizeSelector[] = [], ) { for (let cssSelector of selectorList) { - // Ignore `:is()`, and just process its selectors - if (isIsPseudoClass(cssSelector)) { - getSelectors(cssSelector[0].selectors, isDarkMode, options, selectors); + // 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) @@ -110,7 +111,7 @@ export function getSelectors( // ["=", "prefers-color-scheme", "dark"], // ]); } else { - const selector = classNameSelector(cssSelector, options); + const selector = classNameSelector(cssSelector, builder.getOptions()); if (selector === null) { continue; @@ -178,11 +179,12 @@ function classNameSelector( switch (component.type) { case "universal": case "namespace": - case "nesting": 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; @@ -371,6 +373,18 @@ function isIsPseudoClass( ); } +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; diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts index d34e69cc..5d73e916 100644 --- a/src/compiler/stylesheet.ts +++ b/src/compiler/stylesheet.ts @@ -1,3 +1,5 @@ +import type { SelectorList } from "lightningcss"; + import { isStyleDescriptorArray, Specificity, @@ -19,7 +21,7 @@ import type { VariableRecord, VariableValue, } from "./compiler.types"; -import { toRNProperty, type NormalizeSelector } from "./selectors"; +import { getClassNameSelectors, toRNProperty } from "./selector-builder"; type BuilderMode = "style" | "media" | "container" | "keyframes"; @@ -69,14 +71,25 @@ export class StylesheetBuilder { warningValues: {}, warningFunctions: [], }, - private selectors?: NormalizeSelector[], + private selectors: SelectorList = [], ) {} - fork( - mode = this.mode, - selectors: NormalizeSelector[] | undefined = this.selectors, - ): StylesheetBuilder { + fork(mode = this.mode, selectors: SelectorList = []): StylesheetBuilder { this.shared.ruleOrder++; + + /** + * If we already have selectors and we are added more + * Then these must be nested selectors. + * + * We need to extrapolate out the selectors to their full values + */ + selectors = + this.selectors.length && selectors.length + ? this.selectors.flatMap((selectorA) => { + return selectors.map((selectorB) => [...selectorA, ...selectorB]); + }) + : [...this.selectors, ...selectors]; + return new StylesheetBuilder( this.options, mode, @@ -413,7 +426,7 @@ export class StylesheetBuilder { } applyRuleToSelectors(selectorList = this.selectors): void { - if (!selectorList?.length) { + if (!selectorList.length) { // If there are no selectors, we cannot apply the rule return; } @@ -422,7 +435,12 @@ export class StylesheetBuilder { return; } - for (const selector of selectorList) { + const normalizedSelectors = getClassNameSelectors( + selectorList, + this.options, + ); + + for (const selector of normalizedSelectors) { // We are going to be apply the current rule to n selectors, so we clone the rule const rule = this.cloneRule(this.rule); @@ -436,6 +454,10 @@ export class StylesheetBuilder { attributeQuery, } = selector; + if (!className) { + continue; // No className, nothing to do + } + // Combine the specificity of the selector with the rule's specificity for (let i = 0; i < specificity.length; i++) { const spec = specificity[i]; @@ -459,14 +481,29 @@ export class StylesheetBuilder { continue; } + const [first, ...rest] = name.split("."); + + if (typeof first !== "string") { + continue; + } + const containerRule: StyleRule = { // These are not "real" rules, so they use the lowest specificity s: [0], c: [name], }; + if (rest.length) { + containerRule.aq = rest.map((attr) => [ + "a", + "className", + "*=", + attr, + ]); + } + // Create rules for the parent classes - this.addRuleToRuleSet(name, containerRule); + this.addRuleToRuleSet(first, containerRule); } } @@ -495,24 +532,15 @@ export class StylesheetBuilder { if (!this.rule.v) { continue; } - - const { type, subtype } = selector; - + const { type } = selector; for (const [name, value] of this.rule.v) { this.shared[type] ??= {}; this.shared[type][name] ??= []; - - const variableValue: VariableValue = - subtype === "light" - ? [value] - : [value, [["=", "prefers-color-scheme", "dark"]]]; - + const mediaQueries = this.rule.m; + const variableValue: VariableValue = mediaQueries + ? [value, [...mediaQueries]] + : [value]; // Append extra media queries if they exist - if (this.rule.m) { - variableValue[1] ??= []; - variableValue[1].push(...this.rule.m); - } - this.shared[type][name].push(variableValue); } } diff --git a/src/runtime/native/__tests__/grouping.test.tsx b/src/runtime/native/__tests__/grouping.test.tsx index dec5ff1d..3c9f844a 100644 --- a/src/runtime/native/__tests__/grouping.test.tsx +++ b/src/runtime/native/__tests__/grouping.test.tsx @@ -99,13 +99,13 @@ test.skip("group - active (animated)", () => { test("group selector", () => { registerCSS( - `.group.test .my-class { + `.my-a.my-b .my-class { color: red; }`, ); const { rerender } = render( - + , ); @@ -115,7 +115,7 @@ test("group selector", () => { expect(child.props.style).toStrictEqual({ color: "#f00" }); rerender( - + , ); diff --git a/src/runtime/native/conditions/attributes.ts b/src/runtime/native/conditions/attributes.ts new file mode 100644 index 00000000..de8e8817 --- /dev/null +++ b/src/runtime/native/conditions/attributes.ts @@ -0,0 +1,52 @@ +import type { AttributeQuery } from "../../../compiler"; +import type { Props } from "../../runtime.types"; +import type { RenderGuard } from "./guards"; + +export function testAttributes( + queries: AttributeQuery[], + props: Props, + guards: RenderGuard[], +) { + return queries.every((query) => testAttribute(query, props, guards)); +} + +function testAttribute( + [type, prop, operator, testValue]: AttributeQuery, + props: Props, + guards: RenderGuard[], +) { + let value: unknown; + + if (type === "a") { + value = props?.[prop]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + value = props?.dataSet?.[prop]; + } + + guards.push([type, prop, value]); + + if (!operator) { + return value !== undefined && value !== null && value !== false; + } + + switch (operator) { + case "!": + return !value; + case "=": + return value == testValue; + case "~=": + return testValue && value?.toString().split(" ").includes(testValue); + case "|=": + return testValue && value?.toString().startsWith(testValue + "-"); + case "^=": + return testValue && value?.toString().startsWith(testValue); + case "$=": + return testValue && value?.toString().endsWith(testValue); + case "*=": + return testValue && value?.toString().includes(testValue); + default: + operator satisfies never; + return false; + } +} diff --git a/src/runtime/native/conditions/container-query.ts b/src/runtime/native/conditions/container-query.ts index a165627f..f6bcaf2d 100644 --- a/src/runtime/native/conditions/container-query.ts +++ b/src/runtime/native/conditions/container-query.ts @@ -16,6 +16,7 @@ import { type ContainerContextValue, type Getter, } from "../reactivity"; +// import { testAttributes } from "./attributes"; import type { RenderGuard } from "./guards"; export const DEFAULT_CONTAINER_NAME = "c:___default___"; @@ -46,6 +47,10 @@ export function testContainerQuery( return false; } + // if (query.a && !testAttributes(query.a, container.props, guards)) { + // return false; + // } + if (query.m && !testContainerMediaCondition(query.m, container, get)) { return false; } diff --git a/src/runtime/native/conditions/index.ts b/src/runtime/native/conditions/index.ts index 4ee08295..4240e872 100644 --- a/src/runtime/native/conditions/index.ts +++ b/src/runtime/native/conditions/index.ts @@ -1,9 +1,4 @@ -/* eslint-disable */ -import type { - AttributeQuery, - PseudoClassesQuery, - StyleRule, -} from "../../../compiler"; +import type { PseudoClassesQuery, StyleRule } from "../../../compiler"; import type { Props } from "../../runtime.types"; import { activeFamily, @@ -12,6 +7,7 @@ import { type ContainerContextValue, type Getter, } from "../reactivity"; +import { testAttributes } from "./attributes"; import { testContainerQueries } from "./container-query"; import type { RenderGuard } from "./guards"; import { testMediaQuery } from "./media-query"; @@ -29,7 +25,7 @@ export function testRule( if (rule.m && !testMediaQuery(rule.m, get)) { return false; } - if (rule.aq && !attributes(rule.aq, props, guards)) { + if (rule.aq && !testAttributes(rule.aq, props, guards)) { return false; } if ( @@ -54,53 +50,3 @@ function pseudoClasses(query: PseudoClassesQuery, get: Getter) { } return true; } - -function attributes( - queries: AttributeQuery[], - props: Props, - guards: RenderGuard[], -) { - return queries.every((query) => testAttribute(query, props, guards)); -} - -function testAttribute( - query: AttributeQuery, - props: Props, - guards: RenderGuard[], -) { - let value: any; - - if (query[0] === "a") { - value = props?.[query[1]]; - } else { - value = props?.dataSet?.[query[1]]; - } - - guards.push([query[0], query[1], value]); - - const operator = query[2]; - - if (!operator) { - return value !== undefined && value !== null && value !== false; - } - - switch (operator) { - case "!": - return !value; - case "=": - return value == query[3]; - case "~=": - return value?.toString().split(" ").includes(query[3]); - case "|=": - return value?.toString().startsWith(query[3] + "-"); - case "^=": - return value?.toString().startsWith(query[3]); - case "$=": - return value?.toString().endsWith(query[3]); - case "*=": - return value?.toString().includes(query[3]); - default: - operator satisfies never; - return false; - } -} diff --git a/src/runtime/utils/specificity.ts b/src/runtime/utils/specificity.ts index 5d74f04a..760f6433 100644 --- a/src/runtime/utils/specificity.ts +++ b/src/runtime/utils/specificity.ts @@ -8,6 +8,7 @@ export const Specificity = { Important: 2, Inline: 3, PseudoElements: 4, + PseudoClass: 1, // Id: 0, - We don't support ID yet // StyleSheet: 0, - We don't support multiple stylesheets };