From 9700fdd6e89d5eb95185f340eaf3edf865f635d7 Mon Sep 17 00:00:00 2001 From: 2biazdk Date: Wed, 3 Oct 2018 11:30:26 +0200 Subject: [PATCH 01/22] First stab at anbmicrosyntax (WIP) --- .../alfa-css/src/grammars/anb-microsyntax.ts | 75 +++++++++++++++++++ packages/alfa-css/src/grammars/selector.ts | 27 +++++-- packages/alfa-css/src/types.ts | 11 ++- .../alfa-css/test/grammars/selector.spec.ts | 30 ++++++++ 4 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 packages/alfa-css/src/grammars/anb-microsyntax.ts diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts new file mode 100644 index 0000000000..6a1fc5a41b --- /dev/null +++ b/packages/alfa-css/src/grammars/anb-microsyntax.ts @@ -0,0 +1,75 @@ +import { Stream } from "@siteimprove/alfa-lang"; +import { Token, TokenType } from "../alphabet"; +import { AnBSelector, SelectorType } from "../types"; + +export function AnBMicrosyntax(stream: Stream): AnBSelector | null { + let next = stream.next(); + + if (next === null) { + return null; + } + + let a = 0; + let b = 0; + + if (next.type === TokenType.Ident) { + const oddEven = oddEvenSyntax(next.value); + + if (oddEven !== null) { + return oddEven; + } + + if (next.value !== "n") { + return null; + } + } else { + switch (next.type) { + case TokenType.Number: + // Keep "a" as 0 + break; + case TokenType.Dimension: + a = next.value; + break; + default: + return null; + } + } + + next = stream.next(); + + if (next === null || next.type !== TokenType.Number) { + return null; + } + + b = next.value; + + return { + type: SelectorType.AnBSelector, + a, + b + }; +} + +export function oddEvenSyntax(ident: string): AnBSelector | null { + let a = 0; + let b = 0; + + switch (ident) { + case "odd": + a = 2; + b = 1; + break; + case "even": + a = 2; + b = 0; + break; + default: + return null; + } + + return { + type: SelectorType.AnBSelector, + a, + b + }; +} diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index d2ef22c583..63b6b18ee0 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -26,6 +26,7 @@ import { SimpleSelector, TypeSelector } from "../types"; +import { AnBMicrosyntax } from "./anb-microsyntax"; const { isArray } = Array; @@ -326,16 +327,16 @@ function pseudoSelector( case "root": case "empty": case "blank": - case "nth-child": - case "nth-last-child": case "first-child": case "last-child": case "only-child": - case "nth-of-type": - case "nth-last-of-type": case "first-of-type": case "last-of-type": case "only-of-type": + case "nth-child": + case "nth-last-child": + case "nth-of-type": + case "nth-last-of-type": case "nth-col": case "nth-last-col": name = next.value; @@ -347,7 +348,23 @@ function pseudoSelector( if (next.type === TokenType.Ident) { selector = { type: SelectorType.PseudoClassSelector, name, value: null }; } else { - const value = expression(); + let value = null; + + switch (name) { + case "nth-child": + case "nth-last-child": + case "nth-of-type": + case "nth-last-of-type": + case "nth-col": + case "nth-last-col": + value = AnBMicrosyntax(stream); + if (value === null) { + return null; + } + break; + default: + value = expression(); + } next = stream.next(); diff --git a/packages/alfa-css/src/types.ts b/packages/alfa-css/src/types.ts index 564b41323e..fb08089bab 100644 --- a/packages/alfa-css/src/types.ts +++ b/packages/alfa-css/src/types.ts @@ -36,7 +36,8 @@ export const enum SelectorType { PseudoClassSelector = 16, PseudoElementSelector = 32, CompoundSelector = 64, - RelativeSelector = 128 + RelativeSelector = 128, + AnBSelector = 256 } export interface IdSelector { @@ -106,7 +107,7 @@ export interface TypeSelector { export interface PseudoClassSelector { readonly type: SelectorType.PseudoClassSelector; readonly name: PseudoClass; - readonly value: Selector | Array | null; + readonly value: Selector | Array | AnBSelector | null; } export interface PseudoElementSelector { @@ -295,3 +296,9 @@ export type PseudoElement = | "marker" // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo | "placeholder"; + +export interface AnBSelector { + readonly type: SelectorType.AnBSelector; + readonly a: number; + readonly b: number; +} diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 106d79671a..2034b01a69 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -811,3 +811,33 @@ test("Can parse a relative selector relative to a compound selector", t => { } }); }); + +test("Can parse selector with a An+B odd microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + type: SelectorType.AnBSelector, + a: 2, + b: 1 + } + }; + selector(t, ":nth-child(2n+1)", expected); + selector(t, ":nth-child(odd)", expected); + // selector(t, ":nth-child( odd )", expected); // Solve with split() +}); + +test("Can parse selector with a An+B even microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + type: SelectorType.AnBSelector, + a: 2, + b: 0 + } + }; + selector(t, ":nth-child(2n+0)", expected); + selector(t, ":nth-child(even)", expected); + // selector(t, ":nth-child( even )", expected); // Solve with split() +}); From 06cd9593483c9edc8bf487452b3f8c4918088f63 Mon Sep 17 00:00:00 2001 From: 2biazdk Date: Wed, 28 Nov 2018 12:50:53 +0100 Subject: [PATCH 02/22] Add AnBMicrosyntax as a pseudoclass value --- .../alfa-css/src/grammars/anb-microsyntax.ts | 148 +- packages/alfa-css/src/types.ts | 716 +++---- .../alfa-css/test/grammars/selector.spec.ts | 1684 ++++++++--------- packages/alfa-dom/src/matches.ts | 1500 +++++++-------- 4 files changed, 2030 insertions(+), 2018 deletions(-) diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts index 6a1fc5a41b..0b6005bd4a 100644 --- a/packages/alfa-css/src/grammars/anb-microsyntax.ts +++ b/packages/alfa-css/src/grammars/anb-microsyntax.ts @@ -1,75 +1,73 @@ -import { Stream } from "@siteimprove/alfa-lang"; -import { Token, TokenType } from "../alphabet"; -import { AnBSelector, SelectorType } from "../types"; - -export function AnBMicrosyntax(stream: Stream): AnBSelector | null { - let next = stream.next(); - - if (next === null) { - return null; - } - - let a = 0; - let b = 0; - - if (next.type === TokenType.Ident) { - const oddEven = oddEvenSyntax(next.value); - - if (oddEven !== null) { - return oddEven; - } - - if (next.value !== "n") { - return null; - } - } else { - switch (next.type) { - case TokenType.Number: - // Keep "a" as 0 - break; - case TokenType.Dimension: - a = next.value; - break; - default: - return null; - } - } - - next = stream.next(); - - if (next === null || next.type !== TokenType.Number) { - return null; - } - - b = next.value; - - return { - type: SelectorType.AnBSelector, - a, - b - }; -} - -export function oddEvenSyntax(ident: string): AnBSelector | null { - let a = 0; - let b = 0; - - switch (ident) { - case "odd": - a = 2; - b = 1; - break; - case "even": - a = 2; - b = 0; - break; - default: - return null; - } - - return { - type: SelectorType.AnBSelector, - a, - b - }; -} +import { Stream } from "@siteimprove/alfa-lang"; +import { Token, TokenType } from "../alphabet"; +import { AnBMicrosyntax } from "../types"; + +export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { + let next = stream.next(); + + if (next === null) { + return null; + } + + let a = 0; + let b = 0; + + if (next.type === TokenType.Ident) { + const oddEven = oddEvenSyntax(next.value); + + if (oddEven !== null) { + return oddEven; + } + + if (next.value !== "n") { + return null; + } + } else { + switch (next.type) { + case TokenType.Number: + // Keep "a" as 0 + break; + case TokenType.Dimension: + a = next.value; + break; + default: + return null; + } + } + + next = stream.next(); + + if (next === null || next.type !== TokenType.Number) { + return null; + } + + b = next.value; + + return { + a, + b + }; +} + +export function oddEvenSyntax(ident: string): AnBMicrosyntax | null { + let a = 0; + let b = 0; + + switch (ident) { + case "odd": + a = 2; + b = 1; + break; + case "even": + a = 2; + b = 0; + break; + default: + return null; + } + + return { + a, + b + }; +} diff --git a/packages/alfa-css/src/types.ts b/packages/alfa-css/src/types.ts index 3133098fed..23bd4e269a 100644 --- a/packages/alfa-css/src/types.ts +++ b/packages/alfa-css/src/types.ts @@ -1,357 +1,359 @@ -import { Token } from "./alphabet"; -import { Values } from "./values"; - -/** - * @see https://www.w3.org/TR/css-syntax/#declaration - */ -export interface Declaration { - readonly name: string; - readonly value: Array; - readonly important: boolean; -} - -/** - * @see https://www.w3.org/TR/css-syntax/#at-rule - */ -export interface AtRule { - readonly name: string; - readonly prelude: Array; - readonly value?: Array; -} - -/** - * @see https://www.w3.org/TR/css-syntax/#qualified-rule - */ -export interface QualifiedRule { - readonly prelude: Array; - readonly value: Array; -} - -export type Rule = AtRule | QualifiedRule; - -export const enum SelectorType { - IdSelector = 1, - ClassSelector = 2, - AttributeSelector = 4, - TypeSelector = 8, - PseudoClassSelector = 16, - PseudoElementSelector = 32, - CompoundSelector = 64, - RelativeSelector = 128, - AnBSelector = 256 -} - -export interface IdSelector { - readonly type: SelectorType.IdSelector; - readonly name: string; -} - -export interface ClassSelector { - readonly type: SelectorType.ClassSelector; - readonly name: string; -} - -export const enum AttributeMatcher { - /** - * @example [foo=bar] - */ - Equal, - - /** - * @example [foo~=bar] - */ - Includes, - - /** - * @example [foo|=bar] - */ - DashMatch, - - /** - * @example [foo^=bar] - */ - Prefix, - - /** - * @example [foo$=bar] - */ - Suffix, - - /** - * @example [foo*=bar] - */ - Substring -} - -export const enum AttributeModifier { - /** - * @example [foo=bar i] - */ - CaseInsensitive = 1 -} - -export interface AttributeSelector { - readonly type: SelectorType.AttributeSelector; - readonly name: string; - readonly namespace: string | null; - readonly value: string | null; - readonly matcher: AttributeMatcher | null; - readonly modifier: number; -} - -export interface TypeSelector { - readonly type: SelectorType.TypeSelector; - readonly name: string; - readonly namespace: string | null; -} - -export interface PseudoClassSelector { - readonly type: SelectorType.PseudoClassSelector; - readonly name: PseudoClass; - readonly value: Selector | Array | AnBSelector | null; -} - -export interface PseudoElementSelector { - readonly type: SelectorType.PseudoElementSelector; - readonly name: PseudoElement; -} - -export type SimpleSelector = - | IdSelector - | ClassSelector - | TypeSelector - | AttributeSelector - | PseudoClassSelector - | PseudoElementSelector; - -export interface CompoundSelector { - readonly type: SelectorType.CompoundSelector; - readonly left: SimpleSelector; - readonly right: SimpleSelector | CompoundSelector; -} - -export type ComplexSelector = SimpleSelector | CompoundSelector; - -export const enum SelectorCombinator { - /** - * @example div span - */ - Descendant, - - /** - * @example div > span - */ - DirectDescendant, - - /** - * @example div ~ span - */ - Sibling, - - /** - * @example div + span - */ - DirectSibling -} - -export interface RelativeSelector { - readonly type: SelectorType.RelativeSelector; - readonly combinator: SelectorCombinator; - readonly left: ComplexSelector | RelativeSelector; - readonly right: ComplexSelector; -} - -export type Selector = ComplexSelector | RelativeSelector; - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-classes - */ -export type PseudoClass = - // https://www.w3.org/TR/selectors/#matches-pseudo - | "matches" - // https://www.w3.org/TR/selectors/#negation-pseudo - | "not" - // https://www.w3.org/TR/selectors/#something-pseudo - | "something" - // https://www.w3.org/TR/selectors/#has-pseudo - | "has" - // https://www.w3.org/TR/selectors/#dir-pseudo - | "dir" - // https://www.w3.org/TR/selectors/#lang-pseudo - | "lang" - // https://www.w3.org/TR/selectors/#any-link-pseudo - | "any-link" - // https://www.w3.org/TR/selectors/#link-pseudo - | "link" - // https://www.w3.org/TR/selectors/#visited-pseudo - | "visited" - // https://www.w3.org/TR/selectors/#local-link-pseudo - | "local-link" - // https://www.w3.org/TR/selectors/#target-pseudo - | "target" - // https://www.w3.org/TR/selectors/#target-within-pseudo - | "target-within" - // https://www.w3.org/TR/selectors/#scope-pseudo - | "scope" - // https://www.w3.org/TR/selectors/#hover-pseudo - | "hover" - // https://www.w3.org/TR/selectors/#active-pseudo - | "active" - // https://www.w3.org/TR/selectors/#focus-pseudo - | "focus" - // https://www.w3.org/TR/selectors/#focus-visible-pseudo - | "focus-visible" - // https://www.w3.org/TR/selectors/#focus-within-pseudo - | "focus-within" - // https://www.w3.org/TR/selectors/#drag-pseudos - | "drop" - // https://www.w3.org/TR/selectors/#current-pseudo - | "current" - // https://www.w3.org/TR/selectors/#past-pseudo - | "past" - // https://www.w3.org/TR/selectors/#future-pseudo - | "future" - // https://www.w3.org/TR/selectors/#video-state - | "playing" - | "paused" - // https://www.w3.org/TR/selectors/#enabled-pseudo - | "enabled" - // https://www.w3.org/TR/selectors/#disabled-pseudo - | "disabled" - // https://www.w3.org/TR/selectors/#read-only-pseudo - | "read-only" - // https://www.w3.org/TR/selectors/#read-write-pseudo - | "read-write" - // https://www.w3.org/TR/selectors/#placeholder-shown-pseudo - | "placeholder-shown" - // https://www.w3.org/TR/selectors/#default-pseudo - | "default" - // https://www.w3.org/TR/selectors/#checked-pseudo - | "checked" - // https://www.w3.org/TR/selectors/#indetermine-pseudo - | "indetermine" - // https://www.w3.org/TR/selectors/#valid-pseudo - | "valid" - // https://www.w3.org/TR/selectors/#invalid-pseudo - | "invalid" - // https://www.w3.org/TR/selectors/#in-range-pseudo - | "in-range" - // https://www.w3.org/TR/selectors/#out-of-range-pseudo - | "out-of-range" - // https://www.w3.org/TR/selectors/#required-pseudo - | "required" - // https://www.w3.org/TR/selectors/#user-invalid-pseudo - | "user-invalid" - // https://drafts.csswg.org/css-scoping/#host-selector - | "host" - // https://drafts.csswg.org/css-scoping/#host-selector - | "host-context" - // https://www.w3.org/TR/selectors/#root-pseudo - | "root" - // https://www.w3.org/TR/selectors/#empty-pseudo - | "empty" - // https://www.w3.org/TR/selectors/#blank-pseudo - | "blank" - // https://www.w3.org/TR/selectors/#nth-child-pseudo - | "nth-child" - // https://www.w3.org/TR/selectors/#nth-last-child-pseudo - | "nth-last-child" - // https://www.w3.org/TR/selectors/#first-child-pseudo - | "first-child" - // https://www.w3.org/TR/selectors/#last-child-pseudo - | "last-child" - // https://www.w3.org/TR/selectors/#only-child-pseudo - | "only-child" - // https://www.w3.org/TR/selectors/#nth-of-type-pseudo - | "nth-of-type" - // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo - | "nth-last-of-type" - // https://www.w3.org/TR/selectors/#first-of-type-pseudo - | "first-of-type" - // https://www.w3.org/TR/selectors/#last-of-type-pseudo - | "last-of-type" - // https://www.w3.org/TR/selectors/#only-of-type-pseudo - | "only-of-type" - // https://www.w3.org/TR/selectors/#nth-col-pseudo - | "nth-col" - // https://www.w3.org/TR/selectors/#nth-last-col-pseudo - | "nth-last-col"; - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-elements - */ -export type PseudoElement = - // https://www.w3.org/TR/css-pseudo/#first-line-pseudo - | "first-line" - // https://www.w3.org/TR/css-pseudo/#first-letter-pseudo - | "first-letter" - // https://www.w3.org/TR/css-pseudo/#highlight-pseudos - | "selection" - | "inactive-selection" - | "spelling-error" - | "grammar-error" - // https://www.w3.org/TR/css-pseudo/#generated-content - | "before" - | "after" - // https://www.w3.org/TR/css-pseudo/#marker-pseudo - | "marker" - // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo - | "placeholder"; - -export interface AnBSelector { - readonly type: SelectorType.AnBSelector; - readonly a: number; - readonly b: number; -} - -export const enum MediaQualifier { - Only, - Not -} - -export const enum MediaOperator { - Not, - And, - Or -} - -export const enum MediaComparator { - GreaterThan, - GreaterThanEqual, - LessThan, - LessTahnEqual -} - -export type MediaType = string; - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-query - */ -export interface MediaQuery { - readonly qualifier?: MediaQualifier; - readonly type?: MediaType; - readonly condition?: MediaCondition; -} - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-condition - */ -export interface MediaCondition { - readonly operator?: MediaOperator; - readonly features: Array; -} - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-feature - */ -export interface MediaFeature { - readonly name: string; - readonly value?: MediaFeatureValue; - readonly comparator?: MediaComparator; -} - -export type MediaFeatureValue = - | Values.Number - | Values.Percentage - | Values.Length - | Values.String; +import { Token } from "./alphabet"; +import { Values } from "./values"; + +/** + * @see https://www.w3.org/TR/css-syntax/#declaration + */ +export interface Declaration { + readonly name: string; + readonly value: Array; + readonly important: boolean; +} + +/** + * @see https://www.w3.org/TR/css-syntax/#at-rule + */ +export interface AtRule { + readonly name: string; + readonly prelude: Array; + readonly value?: Array; +} + +/** + * @see https://www.w3.org/TR/css-syntax/#qualified-rule + */ +export interface QualifiedRule { + readonly prelude: Array; + readonly value: Array; +} + +export type Rule = AtRule | QualifiedRule; + +export const enum SelectorType { + IdSelector = 1, + ClassSelector = 2, + AttributeSelector = 4, + TypeSelector = 8, + PseudoClassSelector = 16, + PseudoElementSelector = 32, + CompoundSelector = 64, + RelativeSelector = 128 +} + +export interface IdSelector { + readonly type: SelectorType.IdSelector; + readonly name: string; +} + +export interface ClassSelector { + readonly type: SelectorType.ClassSelector; + readonly name: string; +} + +export const enum AttributeMatcher { + /** + * @example [foo=bar] + */ + Equal, + + /** + * @example [foo~=bar] + */ + Includes, + + /** + * @example [foo|=bar] + */ + DashMatch, + + /** + * @example [foo^=bar] + */ + Prefix, + + /** + * @example [foo$=bar] + */ + Suffix, + + /** + * @example [foo*=bar] + */ + Substring +} + +export const enum AttributeModifier { + /** + * @example [foo=bar i] + */ + CaseInsensitive = 1 +} + +export interface AttributeSelector { + readonly type: SelectorType.AttributeSelector; + readonly name: string; + readonly namespace: string | null; + readonly value: string | null; + readonly matcher: AttributeMatcher | null; + readonly modifier: number; +} + +export interface TypeSelector { + readonly type: SelectorType.TypeSelector; + readonly name: string; + readonly namespace: string | null; +} + +export interface PseudoClassSelector { + readonly type: SelectorType.PseudoClassSelector; + readonly name: PseudoClass; + readonly value: Selector | Array | AnBMicrosyntax | null; +} + +export interface PseudoElementSelector { + readonly type: SelectorType.PseudoElementSelector; + readonly name: PseudoElement; +} + +export type SimpleSelector = + | IdSelector + | ClassSelector + | TypeSelector + | AttributeSelector + | PseudoClassSelector + | PseudoElementSelector; + +export interface CompoundSelector { + readonly type: SelectorType.CompoundSelector; + readonly left: SimpleSelector; + readonly right: SimpleSelector | CompoundSelector; +} + +export type ComplexSelector = SimpleSelector | CompoundSelector; + +export const enum SelectorCombinator { + /** + * @example div span + */ + Descendant, + + /** + * @example div > span + */ + DirectDescendant, + + /** + * @example div ~ span + */ + Sibling, + + /** + * @example div + span + */ + DirectSibling +} + +export interface RelativeSelector { + readonly type: SelectorType.RelativeSelector; + readonly combinator: SelectorCombinator; + readonly left: ComplexSelector | RelativeSelector; + readonly right: ComplexSelector; +} + +export type Selector = ComplexSelector | RelativeSelector; + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-classes + */ +export type PseudoClass = + // https://www.w3.org/TR/selectors/#matches-pseudo + | "matches" + // https://www.w3.org/TR/selectors/#negation-pseudo + | "not" + // https://www.w3.org/TR/selectors/#something-pseudo + | "something" + // https://www.w3.org/TR/selectors/#has-pseudo + | "has" + // https://www.w3.org/TR/selectors/#dir-pseudo + | "dir" + // https://www.w3.org/TR/selectors/#lang-pseudo + | "lang" + // https://www.w3.org/TR/selectors/#any-link-pseudo + | "any-link" + // https://www.w3.org/TR/selectors/#link-pseudo + | "link" + // https://www.w3.org/TR/selectors/#visited-pseudo + | "visited" + // https://www.w3.org/TR/selectors/#local-link-pseudo + | "local-link" + // https://www.w3.org/TR/selectors/#target-pseudo + | "target" + // https://www.w3.org/TR/selectors/#target-within-pseudo + | "target-within" + // https://www.w3.org/TR/selectors/#scope-pseudo + | "scope" + // https://www.w3.org/TR/selectors/#hover-pseudo + | "hover" + // https://www.w3.org/TR/selectors/#active-pseudo + | "active" + // https://www.w3.org/TR/selectors/#focus-pseudo + | "focus" + // https://www.w3.org/TR/selectors/#focus-visible-pseudo + | "focus-visible" + // https://www.w3.org/TR/selectors/#focus-within-pseudo + | "focus-within" + // https://www.w3.org/TR/selectors/#drag-pseudos + | "drop" + // https://www.w3.org/TR/selectors/#current-pseudo + | "current" + // https://www.w3.org/TR/selectors/#past-pseudo + | "past" + // https://www.w3.org/TR/selectors/#future-pseudo + | "future" + // https://www.w3.org/TR/selectors/#video-state + | "playing" + | "paused" + // https://www.w3.org/TR/selectors/#enabled-pseudo + | "enabled" + // https://www.w3.org/TR/selectors/#disabled-pseudo + | "disabled" + // https://www.w3.org/TR/selectors/#read-only-pseudo + | "read-only" + // https://www.w3.org/TR/selectors/#read-write-pseudo + | "read-write" + // https://www.w3.org/TR/selectors/#placeholder-shown-pseudo + | "placeholder-shown" + // https://www.w3.org/TR/selectors/#default-pseudo + | "default" + // https://www.w3.org/TR/selectors/#checked-pseudo + | "checked" + // https://www.w3.org/TR/selectors/#indetermine-pseudo + | "indetermine" + // https://www.w3.org/TR/selectors/#valid-pseudo + | "valid" + // https://www.w3.org/TR/selectors/#invalid-pseudo + | "invalid" + // https://www.w3.org/TR/selectors/#in-range-pseudo + | "in-range" + // https://www.w3.org/TR/selectors/#out-of-range-pseudo + | "out-of-range" + // https://www.w3.org/TR/selectors/#required-pseudo + | "required" + // https://www.w3.org/TR/selectors/#user-invalid-pseudo + | "user-invalid" + // https://drafts.csswg.org/css-scoping/#host-selector + | "host" + // https://drafts.csswg.org/css-scoping/#host-selector + | "host-context" + // https://www.w3.org/TR/selectors/#root-pseudo + | "root" + // https://www.w3.org/TR/selectors/#empty-pseudo + | "empty" + // https://www.w3.org/TR/selectors/#blank-pseudo + | "blank" + // https://www.w3.org/TR/selectors/#first-child-pseudo + | "first-child" + // https://www.w3.org/TR/selectors/#last-child-pseudo + | "last-child" + // https://www.w3.org/TR/selectors/#only-child-pseudo + | "only-child" + // https://www.w3.org/TR/selectors/#first-of-type-pseudo + | "first-of-type" + // https://www.w3.org/TR/selectors/#last-of-type-pseudo + | "last-of-type" + // https://www.w3.org/TR/selectors/#only-of-type-pseudo + | "only-of-type" + // https://www.w3.org/TR/selectors/#child-index + | ChildIndexedPseudoClass; + +type ChildIndexedPseudoClass = + // https://www.w3.org/TR/selectors/#nth-child-pseudo + | "nth-child" + // https://www.w3.org/TR/selectors/#nth-last-child-pseudo + | "nth-last-child" + // https://www.w3.org/TR/selectors/#nth-of-type-pseudo + | "nth-of-type" + // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo + | "nth-last-of-type" + // https://www.w3.org/TR/selectors/#nth-col-pseudo + | "nth-col" + // https://www.w3.org/TR/selectors/#nth-last-col-pseudo + | "nth-last-col"; + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-elements + */ +export type PseudoElement = + // https://www.w3.org/TR/css-pseudo/#first-line-pseudo + | "first-line" + // https://www.w3.org/TR/css-pseudo/#first-letter-pseudo + | "first-letter" + // https://www.w3.org/TR/css-pseudo/#highlight-pseudos + | "selection" + | "inactive-selection" + | "spelling-error" + | "grammar-error" + // https://www.w3.org/TR/css-pseudo/#generated-content + | "before" + | "after" + // https://www.w3.org/TR/css-pseudo/#marker-pseudo + | "marker" + // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo + | "placeholder"; + +export interface AnBMicrosyntax { + readonly a: number; + readonly b: number; +} + +export const enum MediaQualifier { + Only, + Not +} + +export const enum MediaOperator { + Not, + And, + Or +} + +export const enum MediaComparator { + GreaterThan, + GreaterThanEqual, + LessThan, + LessTahnEqual +} + +export type MediaType = string; + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-query + */ +export interface MediaQuery { + readonly qualifier?: MediaQualifier; + readonly type?: MediaType; + readonly condition?: MediaCondition; +} + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-condition + */ +export interface MediaCondition { + readonly operator?: MediaOperator; + readonly features: Array; +} + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-feature + */ +export interface MediaFeature { + readonly name: string; + readonly value?: MediaFeatureValue; + readonly comparator?: MediaComparator; +} + +export type MediaFeatureValue = + | Values.Number + | Values.Percentage + | Values.Length + | Values.String; diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 2034b01a69..9a894355f9 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -1,843 +1,841 @@ -import { lex, parse } from "@siteimprove/alfa-lang"; -import { Assertions, test } from "@siteimprove/alfa-test"; -import { Alphabet } from "../../src/alphabet"; -import { SelectorGrammar } from "../../src/grammars/selector"; -import { - AttributeMatcher, - AttributeModifier, - Selector, - SelectorCombinator, - SelectorType -} from "../../src/types"; - -function selector( - t: Assertions, - input: string, - expected: Selector | Array | null -) { - const lexer = lex(input, Alphabet); - const parser = parse(lexer.result, SelectorGrammar); - - t.deepEqual(parser.result, expected, input); -} - -test("Can parse a type selector", t => { - selector(t, "div", { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }); -}); - -test("Can parse an uppercase type selector", t => { - selector(t, "DIV", { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }); -}); - -test("Can parse a type selector with a namespace", t => { - selector(t, "svg|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "svg" - }); -}); - -test("Can parse a type selector with an empty namespace", t => { - selector(t, "|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "" - }); -}); - -test("Can parse the universal selector with an empty namespace", t => { - selector(t, "|*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: "" - }); -}); - -test("Can parse a type selector with the universal namespace", t => { - selector(t, "*|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "*" - }); -}); - -test("Can parse the universal selector with the universal namespace", t => { - selector(t, "*|*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: "*" - }); -}); - -test("Can parse a class selector", t => { - selector(t, ".foo", { - type: SelectorType.ClassSelector, - name: "foo" - }); -}); - -test("Can parse an ID selector", t => { - selector(t, "#foo", { - type: SelectorType.IdSelector, - name: "foo" - }); -}); - -test("Can parse a compound selector", t => { - selector(t, "#foo.bar", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.IdSelector, - name: "foo" - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - }); -}); - -test("Can parse the universal selector", t => { - selector(t, "*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: null - }); -}); - -test("Can parse a compound selector with a type in prefix position", t => { - selector(t, "div.foo", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a single descendant selector", t => { - selector(t, "div .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a single descendant selector with a right-hand type selector", t => { - selector(t, "div span", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - } - }); -}); - -test("Can parse a double descendant selector", t => { - selector(t, "div .foo #bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - right: { - type: SelectorType.IdSelector, - name: "bar" - } - }); -}); - -test("Can parse a direct descendant selector", t => { - selector(t, "div > .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a sibling selector", t => { - selector(t, "div ~ .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a direct sibling selector", t => { - selector(t, "div + .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectSibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a list of simple selectors", t => { - selector(t, ".foo, .bar, .baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.ClassSelector, - name: "bar" - }, - { - type: SelectorType.ClassSelector, - name: "baz" - } - ]); -}); - -test("Can parse a list of simple and compound selectors", t => { - selector(t, ".foo, #bar.baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.IdSelector, - name: "bar" - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of descendant selectors", t => { - selector(t, "div .foo, span .baz", [ - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of sibling selectors", t => { - selector(t, "div ~ .foo, span ~ .baz", [ - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of selectors with no whitespace", t => { - selector(t, ".foo,.bar,.baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.ClassSelector, - name: "bar" - }, - { - type: SelectorType.ClassSelector, - name: "baz" - } - ]); -}); - -test("Can parse a compound selector relative to a class selector", t => { - selector(t, ".foo div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a compound selector relative to a compound selector", t => { - selector(t, "span.foo div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a descendant selector relative to a sibling selector", t => { - selector(t, "div ~ span .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - } - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse an attribute selector without a value", t => { - selector(t, "[foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with an ident value", t => { - selector(t, "[foo=bar]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a string value", t => { - selector(t, '[foo="bar"]', { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a matcher", t => { - selector(t, "[foo*=bar]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Substring, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a casing modifier", t => { - selector(t, "[foo=bar i]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: AttributeModifier.CaseInsensitive - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[*|foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: "*", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[|foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: "", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar=baz]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: "baz", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar|=baz]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: "baz", - matcher: AttributeMatcher.DashMatch, - modifier: 0 - }); -}); - -test("Can parse an attribute selector when part of a compound selector", t => { - selector(t, ".foo[foo]", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - }); -}); - -test("Can parse an attribute selector when part of a descendant selector", t => { - selector(t, "div [foo]", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - }); -}); - -test("Can parse an attribute selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div[foo]", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - } - }); -}); - -test("Can parse a pseudo-element selector", t => { - selector(t, "::before", { - type: SelectorType.PseudoElementSelector, - name: "before" - }); -}); - -test("Can parse a pseudo-element selector when part of a compound selector", t => { - selector(t, ".foo::before", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - }); -}); - -test("Can parse a pseudo-element selector when part of a descendant selector", t => { - selector(t, "div ::before", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - }); -}); - -test("Can parse a pseudo-element selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div::before", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - } - }); -}); - -test("Only allows pseudo-element selectors as the last selector", t => { - selector(t, "::foo.foo", null); - selector(t, "::foo+foo", null); -}); - -test("Can parse a named pseudo-class selector", t => { - selector(t, ":hover", { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - }); -}); - -test("Can parse a functional pseudo-class selector", t => { - selector(t, ":not(.foo)", { - type: SelectorType.PseudoClassSelector, - name: "not", - value: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a pseudo-class selector when part of a compound selector", t => { - selector(t, "div:hover", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - }); -}); - -test("Can parse a pseudo-class selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div:hover", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - } - }); -}); - -test("Can parse a compound type, class, and pseudo-class selector relative to a class selector", t => { - selector(t, ".foo div.bar:hover", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "bar" - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - } - } - }); -}); - -test("Can parse a simple selector relative to a compound selector", t => { - selector(t, ".foo > div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a relative selector relative to a compound selector", t => { - selector(t, ".foo > .bar + div.baz", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectSibling, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - }); -}); - -test("Can parse selector with a An+B odd microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - type: SelectorType.AnBSelector, - a: 2, - b: 1 - } - }; - selector(t, ":nth-child(2n+1)", expected); - selector(t, ":nth-child(odd)", expected); - // selector(t, ":nth-child( odd )", expected); // Solve with split() -}); - -test("Can parse selector with a An+B even microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - type: SelectorType.AnBSelector, - a: 2, - b: 0 - } - }; - selector(t, ":nth-child(2n+0)", expected); - selector(t, ":nth-child(even)", expected); - // selector(t, ":nth-child( even )", expected); // Solve with split() -}); +import { lex, parse } from "@siteimprove/alfa-lang"; +import { Assertions, test } from "@siteimprove/alfa-test"; +import { Alphabet } from "../../src/alphabet"; +import { SelectorGrammar } from "../../src/grammars/selector"; +import { + AttributeMatcher, + AttributeModifier, + Selector, + SelectorCombinator, + SelectorType +} from "../../src/types"; + +function selector( + t: Assertions, + input: string, + expected: Selector | Array | null +) { + const lexer = lex(input, Alphabet); + const parser = parse(lexer.result, SelectorGrammar); + + t.deepEqual(parser.result, expected, input); +} + +test("Can parse a type selector", t => { + selector(t, "div", { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }); +}); + +test("Can parse an uppercase type selector", t => { + selector(t, "DIV", { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }); +}); + +test("Can parse a type selector with a namespace", t => { + selector(t, "svg|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "svg" + }); +}); + +test("Can parse a type selector with an empty namespace", t => { + selector(t, "|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "" + }); +}); + +test("Can parse the universal selector with an empty namespace", t => { + selector(t, "|*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: "" + }); +}); + +test("Can parse a type selector with the universal namespace", t => { + selector(t, "*|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "*" + }); +}); + +test("Can parse the universal selector with the universal namespace", t => { + selector(t, "*|*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: "*" + }); +}); + +test("Can parse a class selector", t => { + selector(t, ".foo", { + type: SelectorType.ClassSelector, + name: "foo" + }); +}); + +test("Can parse an ID selector", t => { + selector(t, "#foo", { + type: SelectorType.IdSelector, + name: "foo" + }); +}); + +test("Can parse a compound selector", t => { + selector(t, "#foo.bar", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.IdSelector, + name: "foo" + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + }); +}); + +test("Can parse the universal selector", t => { + selector(t, "*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: null + }); +}); + +test("Can parse a compound selector with a type in prefix position", t => { + selector(t, "div.foo", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a single descendant selector", t => { + selector(t, "div .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a single descendant selector with a right-hand type selector", t => { + selector(t, "div span", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + } + }); +}); + +test("Can parse a double descendant selector", t => { + selector(t, "div .foo #bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + right: { + type: SelectorType.IdSelector, + name: "bar" + } + }); +}); + +test("Can parse a direct descendant selector", t => { + selector(t, "div > .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a sibling selector", t => { + selector(t, "div ~ .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a direct sibling selector", t => { + selector(t, "div + .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectSibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a list of simple selectors", t => { + selector(t, ".foo, .bar, .baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.ClassSelector, + name: "bar" + }, + { + type: SelectorType.ClassSelector, + name: "baz" + } + ]); +}); + +test("Can parse a list of simple and compound selectors", t => { + selector(t, ".foo, #bar.baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.IdSelector, + name: "bar" + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of descendant selectors", t => { + selector(t, "div .foo, span .baz", [ + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of sibling selectors", t => { + selector(t, "div ~ .foo, span ~ .baz", [ + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of selectors with no whitespace", t => { + selector(t, ".foo,.bar,.baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.ClassSelector, + name: "bar" + }, + { + type: SelectorType.ClassSelector, + name: "baz" + } + ]); +}); + +test("Can parse a compound selector relative to a class selector", t => { + selector(t, ".foo div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a compound selector relative to a compound selector", t => { + selector(t, "span.foo div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a descendant selector relative to a sibling selector", t => { + selector(t, "div ~ span .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + } + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse an attribute selector without a value", t => { + selector(t, "[foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with an ident value", t => { + selector(t, "[foo=bar]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a string value", t => { + selector(t, '[foo="bar"]', { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a matcher", t => { + selector(t, "[foo*=bar]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Substring, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a casing modifier", t => { + selector(t, "[foo=bar i]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: AttributeModifier.CaseInsensitive + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[*|foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: "*", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[|foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: "", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar=baz]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: "baz", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar|=baz]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: "baz", + matcher: AttributeMatcher.DashMatch, + modifier: 0 + }); +}); + +test("Can parse an attribute selector when part of a compound selector", t => { + selector(t, ".foo[foo]", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + }); +}); + +test("Can parse an attribute selector when part of a descendant selector", t => { + selector(t, "div [foo]", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + }); +}); + +test("Can parse an attribute selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div[foo]", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + } + }); +}); + +test("Can parse a pseudo-element selector", t => { + selector(t, "::before", { + type: SelectorType.PseudoElementSelector, + name: "before" + }); +}); + +test("Can parse a pseudo-element selector when part of a compound selector", t => { + selector(t, ".foo::before", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + }); +}); + +test("Can parse a pseudo-element selector when part of a descendant selector", t => { + selector(t, "div ::before", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + }); +}); + +test("Can parse a pseudo-element selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div::before", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + } + }); +}); + +test("Only allows pseudo-element selectors as the last selector", t => { + selector(t, "::foo.foo", null); + selector(t, "::foo+foo", null); +}); + +test("Can parse a named pseudo-class selector", t => { + selector(t, ":hover", { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + }); +}); + +test("Can parse a functional pseudo-class selector", t => { + selector(t, ":not(.foo)", { + type: SelectorType.PseudoClassSelector, + name: "not", + value: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a pseudo-class selector when part of a compound selector", t => { + selector(t, "div:hover", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + }); +}); + +test("Can parse a pseudo-class selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div:hover", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + } + }); +}); + +test("Can parse a compound type, class, and pseudo-class selector relative to a class selector", t => { + selector(t, ".foo div.bar:hover", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "bar" + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + } + } + }); +}); + +test("Can parse a simple selector relative to a compound selector", t => { + selector(t, ".foo > div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a relative selector relative to a compound selector", t => { + selector(t, ".foo > .bar + div.baz", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectSibling, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + }); +}); + +test("Can parse selector with a An+B odd microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 1 + } + }; + selector(t, ":nth-child(2n+1)", expected); + selector(t, ":nth-child(odd)", expected); + // selector(t, ":nth-child( odd )", expected); // Solve with split() +}); + +test("Can parse selector with a An+B even microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 0 + } + }; + selector(t, ":nth-child(2n+0)", expected); + selector(t, ":nth-child(even)", expected); + // selector(t, ":nth-child( even )", expected); // Solve with split() +}); diff --git a/packages/alfa-dom/src/matches.ts b/packages/alfa-dom/src/matches.ts index e8e82e4cd4..29181d43e3 100644 --- a/packages/alfa-dom/src/matches.ts +++ b/packages/alfa-dom/src/matches.ts @@ -1,743 +1,757 @@ -import { - AttributeMatcher, - AttributeModifier, - AttributeSelector, - ClassSelector, - CompoundSelector, - IdSelector, - parseSelector, - PseudoClassSelector, - PseudoElementSelector, - RelativeSelector, - Selector, - SelectorCombinator, - SelectorType, - TypeSelector -} from "@siteimprove/alfa-css"; -import { AncestorFilter } from "./ancestor-filter"; -import { contains } from "./contains"; -import { getAttribute } from "./get-attribute"; -import { getAttributeNamespace } from "./get-attribute-namespace"; -import { getClosest } from "./get-closest"; -import { getElementNamespace } from "./get-element-namespace"; -import { getId } from "./get-id"; -import { getParentElement } from "./get-parent-element"; -import { getPreviousElementSibling } from "./get-previous-element-sibling"; -import { isElement, isShadowRoot } from "./guards"; -import { hasClass } from "./has-class"; -import { Element, Namespace, Node } from "./types"; - -const { isArray } = Array; - -export type MatchesOptions = Readonly<{ - composed?: boolean; - flattened?: boolean; - - /** - * @see https://www.w3.org/TR/selectors/#scope-element - * @internal - */ - scope?: Element; - - /** - * @see https://drafts.csswg.org/css-scoping/#tree-context - * @internal - */ - treeContext?: Node; - - /** - * @see https://www.w3.org/TR/selectors/#the-hover-pseudo - * @internal - */ - hover?: Element | boolean; - - /** - * @see https://www.w3.org/TR/selectors/#the-active-pseudo - * @internal - */ - active?: Element | boolean; - - /** - * @see https://www.w3.org/TR/selectors/#the-focus-pseudo - * @internal - */ - focus?: Element | boolean; - - /** - * Whether or not to perform selector matching against pseudo-elements. - * - * @see https://www.w3.org/TR/selectors/#pseudo-elements - * @internal - */ - pseudo?: boolean; - - /** - * Ancestor filter used for fast-rejecting elements during selector matching. - * - * @internal - */ - filter?: AncestorFilter; - - /** - * Declared prefixes mapped to namespace URI. - * - * @see https://www.w3.org/TR/selectors/#type-nmsp - * @internal - */ - namespaces?: Map; -}>; - -/** - * Given an element and a context, check if the element matches the given - * selector within the context. - * - * @see https://www.w3.org/TR/dom41/#dom-element-matches - */ -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options?: MatchesOptions -): boolean; - -/** - * @internal - */ -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options: MatchesOptions, - root: Selector -): boolean; - -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options: MatchesOptions = {}, - root: Selector | null = null -): boolean { - if (typeof selector === "string") { - const parsed = parseSelector(selector); - - if (parsed === null) { - return false; - } - - selector = parsed; - } - - if (isArray(selector)) { - for (let i = 0, n = selector.length; i < n; i++) { - const root = selector[i]; - - if (matches(element, context, root, options, root)) { - return true; - } - } - - return false; - } - - const { namespaces } = options; - - // If selector is not targetting Type or Attribute, then abort if it does not - // match the default namespace. - if ( - selector.type !== SelectorType.TypeSelector && - selector.type !== SelectorType.AttributeSelector && - namespaces !== undefined - ) { - const namespaceDefault = namespaces.get(null); - if ( - namespaceDefault !== undefined && - getElementNamespace(element, context) !== namespaceDefault - ) { - return false; - } - } - - if (root === null) { - root = selector; - } - - switch (selector.type) { - case SelectorType.IdSelector: - return matchesId(element, selector); - - case SelectorType.ClassSelector: - return matchesClass(element, selector); - - case SelectorType.TypeSelector: - return matchesType(element, context, selector, options); - - case SelectorType.AttributeSelector: - return matchesAttribute(element, context, selector, options); - - case SelectorType.CompoundSelector: - return matchesCompound(element, context, selector, options, root); - - case SelectorType.RelativeSelector: - return matchesRelative(element, context, selector, options, root); - - case SelectorType.PseudoClassSelector: - return matchesPseudoClass(element, context, selector, options, root); - - case SelectorType.PseudoElementSelector: - return matchesPseudoElement(element, context, selector, options, root); - } -} - -/** - * @see https://www.w3.org/TR/selectors/#id-selectors - */ -function matchesId(element: Element, selector: IdSelector): boolean { - return getId(element) === selector.name; -} - -/** - * @see https://www.w3.org/TR/selectors/#class-html - */ -function matchesClass(element: Element, selector: ClassSelector): boolean { - return hasClass(element, selector.name); -} - -/** - * @see https://www.w3.org/TR/selectors/#type-selectors - */ -function matchesType( - element: Element, - context: Node, - selector: TypeSelector, - options: MatchesOptions -): boolean { - // https://www.w3.org/TR/selectors/#the-universal-selector - if (selector.name === "*") { - return true; - } - - if (!matchesElementNamespace(element, context, selector, options)) { - return false; - } - - return element.localName === selector.name; -} - -/** - * @see https://www.w3.org/TR/selectors/#type-nmsp - */ -function matchesElementNamespace( - element: Element, - context: Node, - selector: TypeSelector, - options: MatchesOptions -): boolean { - if (selector.namespace === "*") { - return true; - } - - if (options.namespaces === undefined && selector.namespace === null) { - return true; - } - - const elementNamespace = getElementNamespace(element, context); - - if (selector.namespace === "" && elementNamespace === null) { - return true; - } - - if (options.namespaces === undefined) { - return false; - } - - const declaredNamespace = options.namespaces.get(selector.namespace); - - return ( - declaredNamespace === undefined || elementNamespace === declaredNamespace - ); -} - -const whitespace = /\s+/; - -/** - * @see https://www.w3.org/TR/selectors/#attribute-selectors - */ -function matchesAttribute( - element: Element, - context: Node, - selector: AttributeSelector, - options: MatchesOptions -): boolean { - if (!matchesAttributeNamespace(element, context, selector, options)) { - return false; - } - - let value = null; - const attributeOptions = { - lowerCase: (selector.modifier & AttributeModifier.CaseInsensitive) !== 0 - }; - - switch (selector.namespace) { - case null: - case "": - value = getAttribute(element, selector.name, attributeOptions); - break; - case "*": - value = getAttribute( - element, - context, - selector.name, - "*", - attributeOptions - ); - break; - default: - // Abort when no namespace is declared - if (options.namespaces === undefined) { - return false; - } - // Selector namespace must match a declared namespace - const declaredNamespace = options.namespaces.get(selector.namespace); - - if (declaredNamespace === undefined) { - return false; - } - - value = getAttribute( - element, - context, - selector.name, - declaredNamespace, - attributeOptions - ); - } - - if (value === null) { - return false; - } - - if (Array.isArray(value)) { - for (let i = 0, n = value.length; i < n; i++) { - if (matchesValue(value[i], selector)) { - return true; - } - } - - return false; - } - - return matchesValue(value, selector); -} - -function matchesValue(value: string, selector: AttributeSelector): boolean { - if (selector.value === null) { - return true; - } - - switch (selector.matcher) { - case AttributeMatcher.Equal: - return selector.value === value; - - case AttributeMatcher.Prefix: - return value.startsWith(selector.value); - - case AttributeMatcher.Suffix: - return value.endsWith(selector.value); - - case AttributeMatcher.Substring: - return value.includes(selector.value); - - case AttributeMatcher.DashMatch: - return value === selector.value || value.startsWith(`${selector.value}-`); - - case AttributeMatcher.Includes: - const parts = value.split(whitespace); - for (let i = 0, n = parts.length; i < n; i++) { - if (parts[i] === selector.value) { - return true; - } - } - return false; - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#attrnmsp - */ -function matchesAttributeNamespace( - element: Element, - context: Node, - selector: AttributeSelector, - options: MatchesOptions -): boolean { - if (selector.namespace === null || selector.namespace === "*") { - return true; - } - - const { attributes } = element; - let attribute = null; - - for (let i = 0, n = attributes.length; i < n; i++) { - if (attributes[i].localName !== selector.name) { - continue; - } - - attribute = attributes[i]; - break; - } - - if (attribute === null) { - return false; - } - - const attributeNamespace = getAttributeNamespace(attribute, context); - - // Selector "[|att]" should only match attributes with no namespace - if (selector.namespace === "") { - return attributeNamespace === null; - } - - if (options.namespaces === undefined) { - return false; - } - - return attributeNamespace === options.namespaces.get(selector.namespace); -} - -/** - * @see https://www.w3.org/TR/selectors/#compound - */ -function matchesCompound( - element: Element, - context: Node, - selector: CompoundSelector, - options: MatchesOptions, - root: Selector -): boolean { - if (!matches(element, context, selector.left, options, root)) { - return false; - } - - return matches(element, context, selector.right, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#combinators - */ -function matchesRelative( - element: Element, - context: Node, - selector: RelativeSelector, - options: MatchesOptions, - root: Selector -): boolean { - // Before any other work is done, check if the left part of the selector can - // be rejected by the ancestor filter optionally passed to `matches()`. Only - // descendant and direct-descendant selectors can potentially be rejected. - if (options.filter !== undefined) { - switch (selector.combinator) { - case SelectorCombinator.Descendant: - case SelectorCombinator.DirectDescendant: - if (canReject(selector.left, options.filter)) { - return false; - } - - // If the selector cannot be rejected, unset the ancestor filter as it - // no longer applies when we start recursively moving up the tree. - options = { ...options, filter: undefined }; - } - } - - // Otherwise, make sure that the right part of the selector, i.e. the part - // that relates to the current element, matches. - if (!matches(element, context, selector.right, options, root)) { - return false; - } - - // If it does, move on the heavy part of the work: Looking either up the tree - // for a descendant match or looking to the side of the tree for a sibling - // match. - switch (selector.combinator) { - case SelectorCombinator.Descendant: - return matchesDescendant(element, context, selector.left, options, root); - - case SelectorCombinator.DirectDescendant: - return matchesDirectDescendant( - element, - context, - selector.left, - options, - root - ); - - case SelectorCombinator.Sibling: - return matchesSibling(element, context, selector.left, options, root); - - case SelectorCombinator.DirectSibling: - return matchesDirectSibling( - element, - context, - selector.left, - options, - root - ); - } -} - -/** - * @see https://www.w3.org/TR/selectors/#descendant-combinators - */ -function matchesDescendant( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - let parentElement = getParentElement(element, context, options); - - while (parentElement !== null) { - if (matches(parentElement, context, selector, options, root)) { - return true; - } - - parentElement = getParentElement(parentElement, context, options); - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#child-combinators - */ -function matchesDirectDescendant( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - const parentElement = getParentElement(element, context, options); - - if (parentElement === null) { - return false; - } - - return matches(parentElement, context, selector, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#general-sibling-combinators - */ -function matchesSibling( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - let previousElementSibling = getPreviousElementSibling( - element, - context, - options - ); - - while (previousElementSibling !== null) { - if (matches(previousElementSibling, context, selector, options, root)) { - return true; - } - - previousElementSibling = getPreviousElementSibling( - previousElementSibling, - context, - options - ); - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#adjacent-sibling-combinators - */ -function matchesDirectSibling( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - const previousElementSibling = getPreviousElementSibling( - element, - context, - options - ); - - if (previousElementSibling === null) { - return false; - } - - return matches(previousElementSibling, context, selector, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-classes - */ -function matchesPseudoClass( - element: Element, - context: Node, - selector: PseudoClassSelector, - options: MatchesOptions, - root: Selector -): boolean { - switch (selector.name) { - // https://www.w3.org/TR/selectors/#scope-pseudo - case "scope": - return options.scope === element; - - // https://drafts.csswg.org/css-scoping/#host-selector - case "host": { - // Do not allow prefix (e.g. "div:host") - if (root !== selector) { - return false; - } - - const { treeContext } = options; - - if (treeContext === undefined || !isShadowRoot(treeContext)) { - return false; - } - - const host = getParentElement(treeContext, context, { composed: true }); - - if (host !== element) { - return false; - } - - // Match host with possible selector argument (e.g. ":host(.foo)") - return ( - selector.value === null || - matches(element, context, selector.value, options, root) - ); - } - - case "host-context": { - // Do not allow prefix (e.g. "div:host") - if (root !== selector) { - return false; - } - - const { treeContext } = options; - const query = selector.value; - - if ( - treeContext === undefined || - !isShadowRoot(treeContext) || - query === null - ) { - return false; - } - - const host = getParentElement(treeContext, context, { composed: true }); - - if (host !== element) { - return false; - } - - const predicate = (node: Node) => - isElement(node) && matches(node, context, query, options, root); - - return getClosest(host, context, predicate) !== null; - } - - // https://www.w3.org/TR/selectors/#negation-pseudo - case "not": - return ( - selector.value === null || - !matches(element, context, selector.value, options, root) - ); - - // https://www.w3.org/TR/selectors/#hover-pseudo - case "hover": - const { hover } = options; - - if (hover === undefined || hover === false) { - return false; - } - - return ( - hover === true || contains(element, context, hover, { composed: true }) - ); - - // https://www.w3.org/TR/selectors/#active-pseudo - case "active": - const { active } = options; - - if (active === undefined || active === false) { - return false; - } - - return ( - active === true || - contains(element, context, active, { composed: true }) - ); - - // https://www.w3.org/TR/selectors/#focus-pseudo - case "focus": - const { focus } = options; - - if (focus === undefined || focus === false) { - return false; - } - - return focus === true || element === focus; - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-elements - */ -function matchesPseudoElement( - element: Element, - context: Node, - selector: PseudoElementSelector, - options: MatchesOptions, - root: Selector -): boolean { - return options.pseudo === true; -} - -/** - * Check if a selector can be rejected based on an ancestor filter. - */ -function canReject(selector: Selector, filter: AncestorFilter): boolean { - switch (selector.type) { - case SelectorType.IdSelector: - case SelectorType.ClassSelector: - case SelectorType.TypeSelector: - return !filter.matches(selector); - - case SelectorType.CompoundSelector: - return ( - canReject(selector.left, filter) || canReject(selector.right, filter) - ); - - case SelectorType.RelativeSelector: - const { combinator } = selector; - if ( - combinator === SelectorCombinator.Descendant || - combinator === SelectorCombinator.DirectDescendant - ) { - return ( - canReject(selector.right, filter) || canReject(selector.left, filter) - ); - } - } - - return false; -} +import { + AnBMicrosyntax, + AttributeMatcher, + AttributeModifier, + AttributeSelector, + ClassSelector, + CompoundSelector, + IdSelector, + parseSelector, + PseudoClassSelector, + PseudoElementSelector, + RelativeSelector, + Selector, + SelectorCombinator, + SelectorType, + TypeSelector +} from "@siteimprove/alfa-css"; +import { AncestorFilter } from "./ancestor-filter"; +import { contains } from "./contains"; +import { getAttribute } from "./get-attribute"; +import { getAttributeNamespace } from "./get-attribute-namespace"; +import { getClosest } from "./get-closest"; +import { getElementNamespace } from "./get-element-namespace"; +import { getId } from "./get-id"; +import { getParentElement } from "./get-parent-element"; +import { getPreviousElementSibling } from "./get-previous-element-sibling"; +import { isElement, isShadowRoot } from "./guards"; +import { hasClass } from "./has-class"; +import { Element, Namespace, Node } from "./types"; + +const { isArray } = Array; + +export type MatchesOptions = Readonly<{ + composed?: boolean; + flattened?: boolean; + + /** + * @see https://www.w3.org/TR/selectors/#scope-element + * @internal + */ + scope?: Element; + + /** + * @see https://drafts.csswg.org/css-scoping/#tree-context + * @internal + */ + treeContext?: Node; + + /** + * @see https://www.w3.org/TR/selectors/#the-hover-pseudo + * @internal + */ + hover?: Element | boolean; + + /** + * @see https://www.w3.org/TR/selectors/#the-active-pseudo + * @internal + */ + active?: Element | boolean; + + /** + * @see https://www.w3.org/TR/selectors/#the-focus-pseudo + * @internal + */ + focus?: Element | boolean; + + /** + * Whether or not to perform selector matching against pseudo-elements. + * + * @see https://www.w3.org/TR/selectors/#pseudo-elements + * @internal + */ + pseudo?: boolean; + + /** + * Ancestor filter used for fast-rejecting elements during selector matching. + * + * @internal + */ + filter?: AncestorFilter; + + /** + * Declared prefixes mapped to namespace URI. + * + * @see https://www.w3.org/TR/selectors/#type-nmsp + * @internal + */ + namespaces?: Map; +}>; + +/** + * Given an element and a context, check if the element matches the given + * selector within the context. + * + * @see https://www.w3.org/TR/dom41/#dom-element-matches + */ +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options?: MatchesOptions +): boolean; + +/** + * @internal + */ +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options: MatchesOptions, + root: Selector +): boolean; + +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options: MatchesOptions = {}, + root: Selector | null = null +): boolean { + if (typeof selector === "string") { + const parsed = parseSelector(selector); + + if (parsed === null) { + return false; + } + + selector = parsed; + } + + if (isArray(selector)) { + for (let i = 0, n = selector.length; i < n; i++) { + const root = selector[i]; + + if (matches(element, context, root, options, root)) { + return true; + } + } + + return false; + } + + const { namespaces } = options; + + // If selector is not targetting Type or Attribute, then abort if it does not + // match the default namespace. + if ( + selector.type !== SelectorType.TypeSelector && + selector.type !== SelectorType.AttributeSelector && + namespaces !== undefined + ) { + const namespaceDefault = namespaces.get(null); + if ( + namespaceDefault !== undefined && + getElementNamespace(element, context) !== namespaceDefault + ) { + return false; + } + } + + if (root === null) { + root = selector; + } + + switch (selector.type) { + case SelectorType.IdSelector: + return matchesId(element, selector); + + case SelectorType.ClassSelector: + return matchesClass(element, selector); + + case SelectorType.TypeSelector: + return matchesType(element, context, selector, options); + + case SelectorType.AttributeSelector: + return matchesAttribute(element, context, selector, options); + + case SelectorType.CompoundSelector: + return matchesCompound(element, context, selector, options, root); + + case SelectorType.RelativeSelector: + return matchesRelative(element, context, selector, options, root); + + case SelectorType.PseudoClassSelector: + return matchesPseudoClass(element, context, selector, options, root); + + case SelectorType.PseudoElementSelector: + return matchesPseudoElement(element, context, selector, options, root); + } +} + +/** + * @see https://www.w3.org/TR/selectors/#id-selectors + */ +function matchesId(element: Element, selector: IdSelector): boolean { + return getId(element) === selector.name; +} + +/** + * @see https://www.w3.org/TR/selectors/#class-html + */ +function matchesClass(element: Element, selector: ClassSelector): boolean { + return hasClass(element, selector.name); +} + +/** + * @see https://www.w3.org/TR/selectors/#type-selectors + */ +function matchesType( + element: Element, + context: Node, + selector: TypeSelector, + options: MatchesOptions +): boolean { + // https://www.w3.org/TR/selectors/#the-universal-selector + if (selector.name === "*") { + return true; + } + + if (!matchesElementNamespace(element, context, selector, options)) { + return false; + } + + return element.localName === selector.name; +} + +/** + * @see https://www.w3.org/TR/selectors/#type-nmsp + */ +function matchesElementNamespace( + element: Element, + context: Node, + selector: TypeSelector, + options: MatchesOptions +): boolean { + if (selector.namespace === "*") { + return true; + } + + if (options.namespaces === undefined && selector.namespace === null) { + return true; + } + + const elementNamespace = getElementNamespace(element, context); + + if (selector.namespace === "" && elementNamespace === null) { + return true; + } + + if (options.namespaces === undefined) { + return false; + } + + const declaredNamespace = options.namespaces.get(selector.namespace); + + return ( + declaredNamespace === undefined || elementNamespace === declaredNamespace + ); +} + +const whitespace = /\s+/; + +/** + * @see https://www.w3.org/TR/selectors/#attribute-selectors + */ +function matchesAttribute( + element: Element, + context: Node, + selector: AttributeSelector, + options: MatchesOptions +): boolean { + if (!matchesAttributeNamespace(element, context, selector, options)) { + return false; + } + + let value = null; + const attributeOptions = { + lowerCase: (selector.modifier & AttributeModifier.CaseInsensitive) !== 0 + }; + + switch (selector.namespace) { + case null: + case "": + value = getAttribute(element, selector.name, attributeOptions); + break; + case "*": + value = getAttribute( + element, + context, + selector.name, + "*", + attributeOptions + ); + break; + default: + // Abort when no namespace is declared + if (options.namespaces === undefined) { + return false; + } + // Selector namespace must match a declared namespace + const declaredNamespace = options.namespaces.get(selector.namespace); + + if (declaredNamespace === undefined) { + return false; + } + + value = getAttribute( + element, + context, + selector.name, + declaredNamespace, + attributeOptions + ); + } + + if (value === null) { + return false; + } + + if (Array.isArray(value)) { + for (let i = 0, n = value.length; i < n; i++) { + if (matchesValue(value[i], selector)) { + return true; + } + } + + return false; + } + + return matchesValue(value, selector); +} + +function matchesValue(value: string, selector: AttributeSelector): boolean { + if (selector.value === null) { + return true; + } + + switch (selector.matcher) { + case AttributeMatcher.Equal: + return selector.value === value; + + case AttributeMatcher.Prefix: + return value.startsWith(selector.value); + + case AttributeMatcher.Suffix: + return value.endsWith(selector.value); + + case AttributeMatcher.Substring: + return value.includes(selector.value); + + case AttributeMatcher.DashMatch: + return value === selector.value || value.startsWith(`${selector.value}-`); + + case AttributeMatcher.Includes: + const parts = value.split(whitespace); + for (let i = 0, n = parts.length; i < n; i++) { + if (parts[i] === selector.value) { + return true; + } + } + return false; + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#attrnmsp + */ +function matchesAttributeNamespace( + element: Element, + context: Node, + selector: AttributeSelector, + options: MatchesOptions +): boolean { + if (selector.namespace === null || selector.namespace === "*") { + return true; + } + + const { attributes } = element; + let attribute = null; + + for (let i = 0, n = attributes.length; i < n; i++) { + if (attributes[i].localName !== selector.name) { + continue; + } + + attribute = attributes[i]; + break; + } + + if (attribute === null) { + return false; + } + + const attributeNamespace = getAttributeNamespace(attribute, context); + + // Selector "[|att]" should only match attributes with no namespace + if (selector.namespace === "") { + return attributeNamespace === null; + } + + if (options.namespaces === undefined) { + return false; + } + + return attributeNamespace === options.namespaces.get(selector.namespace); +} + +/** + * @see https://www.w3.org/TR/selectors/#compound + */ +function matchesCompound( + element: Element, + context: Node, + selector: CompoundSelector, + options: MatchesOptions, + root: Selector +): boolean { + if (!matches(element, context, selector.left, options, root)) { + return false; + } + + return matches(element, context, selector.right, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#combinators + */ +function matchesRelative( + element: Element, + context: Node, + selector: RelativeSelector, + options: MatchesOptions, + root: Selector +): boolean { + // Before any other work is done, check if the left part of the selector can + // be rejected by the ancestor filter optionally passed to `matches()`. Only + // descendant and direct-descendant selectors can potentially be rejected. + if (options.filter !== undefined) { + switch (selector.combinator) { + case SelectorCombinator.Descendant: + case SelectorCombinator.DirectDescendant: + if (canReject(selector.left, options.filter)) { + return false; + } + + // If the selector cannot be rejected, unset the ancestor filter as it + // no longer applies when we start recursively moving up the tree. + options = { ...options, filter: undefined }; + } + } + + // Otherwise, make sure that the right part of the selector, i.e. the part + // that relates to the current element, matches. + if (!matches(element, context, selector.right, options, root)) { + return false; + } + + // If it does, move on the heavy part of the work: Looking either up the tree + // for a descendant match or looking to the side of the tree for a sibling + // match. + switch (selector.combinator) { + case SelectorCombinator.Descendant: + return matchesDescendant(element, context, selector.left, options, root); + + case SelectorCombinator.DirectDescendant: + return matchesDirectDescendant( + element, + context, + selector.left, + options, + root + ); + + case SelectorCombinator.Sibling: + return matchesSibling(element, context, selector.left, options, root); + + case SelectorCombinator.DirectSibling: + return matchesDirectSibling( + element, + context, + selector.left, + options, + root + ); + } +} + +/** + * @see https://www.w3.org/TR/selectors/#descendant-combinators + */ +function matchesDescendant( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + let parentElement = getParentElement(element, context, options); + + while (parentElement !== null) { + if (matches(parentElement, context, selector, options, root)) { + return true; + } + + parentElement = getParentElement(parentElement, context, options); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#child-combinators + */ +function matchesDirectDescendant( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + const parentElement = getParentElement(element, context, options); + + if (parentElement === null) { + return false; + } + + return matches(parentElement, context, selector, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#general-sibling-combinators + */ +function matchesSibling( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + let previousElementSibling = getPreviousElementSibling( + element, + context, + options + ); + + while (previousElementSibling !== null) { + if (matches(previousElementSibling, context, selector, options, root)) { + return true; + } + + previousElementSibling = getPreviousElementSibling( + previousElementSibling, + context, + options + ); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#adjacent-sibling-combinators + */ +function matchesDirectSibling( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + const previousElementSibling = getPreviousElementSibling( + element, + context, + options + ); + + if (previousElementSibling === null) { + return false; + } + + return matches(previousElementSibling, context, selector, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-classes + */ +function matchesPseudoClass( + element: Element, + context: Node, + selector: PseudoClassSelector, + options: MatchesOptions, + root: Selector +): boolean { + if (selector.value !== null && isAnBMicrosyntax(selector.value)) { + return false; + } + + switch (selector.name) { + // https://www.w3.org/TR/selectors/#scope-pseudo + case "scope": + return options.scope === element; + + // https://drafts.csswg.org/css-scoping/#host-selector + case "host": { + // Do not allow prefix (e.g. "div:host") + if (root !== selector) { + return false; + } + + const { treeContext } = options; + + if (treeContext === undefined || !isShadowRoot(treeContext)) { + return false; + } + + const host = getParentElement(treeContext, context, { composed: true }); + + if (host !== element) { + return false; + } + + // Match host with possible selector argument (e.g. ":host(.foo)") + return ( + selector.value === null || + matches(element, context, selector.value, options, root) + ); + } + + case "host-context": { + // Do not allow prefix (e.g. "div:host") + if (root !== selector) { + return false; + } + + const { treeContext } = options; + const query = selector.value; + + if ( + treeContext === undefined || + !isShadowRoot(treeContext) || + query === null + ) { + return false; + } + + const host = getParentElement(treeContext, context, { composed: true }); + + if (host !== element) { + return false; + } + + const predicate = (node: Node) => + isElement(node) && matches(node, context, query, options, root); + + return getClosest(host, context, predicate) !== null; + } + + // https://www.w3.org/TR/selectors/#negation-pseudo + case "not": + return ( + selector.value === null || + !matches(element, context, selector.value, options, root) + ); + + // https://www.w3.org/TR/selectors/#hover-pseudo + case "hover": + const { hover } = options; + + if (hover === undefined || hover === false) { + return false; + } + + return ( + hover === true || contains(element, context, hover, { composed: true }) + ); + + // https://www.w3.org/TR/selectors/#active-pseudo + case "active": + const { active } = options; + + if (active === undefined || active === false) { + return false; + } + + return ( + active === true || + contains(element, context, active, { composed: true }) + ); + + // https://www.w3.org/TR/selectors/#focus-pseudo + case "focus": + const { focus } = options; + + if (focus === undefined || focus === false) { + return false; + } + + return focus === true || element === focus; + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-elements + */ +function matchesPseudoElement( + element: Element, + context: Node, + selector: PseudoElementSelector, + options: MatchesOptions, + root: Selector +): boolean { + return options.pseudo === true; +} + +/** + * Check if a selector can be rejected based on an ancestor filter. + */ +function canReject(selector: Selector, filter: AncestorFilter): boolean { + switch (selector.type) { + case SelectorType.IdSelector: + case SelectorType.ClassSelector: + case SelectorType.TypeSelector: + return !filter.matches(selector); + + case SelectorType.CompoundSelector: + return ( + canReject(selector.left, filter) || canReject(selector.right, filter) + ); + + case SelectorType.RelativeSelector: + const { combinator } = selector; + if ( + combinator === SelectorCombinator.Descendant || + combinator === SelectorCombinator.DirectDescendant + ) { + return ( + canReject(selector.right, filter) || canReject(selector.left, filter) + ); + } + } + + return false; +} + +/** + * Check if a selector is of interface AnBMicrosyntax. + */ +function isAnBMicrosyntax( + selector: AnBMicrosyntax | Selector | Array +): selector is AnBMicrosyntax { + return (selector).a !== undefined; +} From 6de1fc132b4bfe05d43f244b41a601f317bdaf16 Mon Sep 17 00:00:00 2001 From: 2biazdk Date: Fri, 30 Nov 2018 13:56:28 +0100 Subject: [PATCH 03/22] Support more test cases --- .../alfa-css/src/grammars/anb-microsyntax.ts | 140 +- packages/alfa-css/src/types.ts | 718 +++---- .../alfa-css/test/grammars/selector.spec.ts | 1756 +++++++++-------- packages/alfa-dom/src/matches.ts | 1518 +++++++------- 4 files changed, 2102 insertions(+), 2030 deletions(-) diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts index 0b6005bd4a..49d7bfcb1b 100644 --- a/packages/alfa-css/src/grammars/anb-microsyntax.ts +++ b/packages/alfa-css/src/grammars/anb-microsyntax.ts @@ -1,73 +1,67 @@ -import { Stream } from "@siteimprove/alfa-lang"; -import { Token, TokenType } from "../alphabet"; -import { AnBMicrosyntax } from "../types"; - -export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { - let next = stream.next(); - - if (next === null) { - return null; - } - - let a = 0; - let b = 0; - - if (next.type === TokenType.Ident) { - const oddEven = oddEvenSyntax(next.value); - - if (oddEven !== null) { - return oddEven; - } - - if (next.value !== "n") { - return null; - } - } else { - switch (next.type) { - case TokenType.Number: - // Keep "a" as 0 - break; - case TokenType.Dimension: - a = next.value; - break; - default: - return null; - } - } - - next = stream.next(); - - if (next === null || next.type !== TokenType.Number) { - return null; - } - - b = next.value; - - return { - a, - b - }; -} - -export function oddEvenSyntax(ident: string): AnBMicrosyntax | null { - let a = 0; - let b = 0; - - switch (ident) { - case "odd": - a = 2; - b = 1; - break; - case "even": - a = 2; - b = 0; - break; - default: - return null; - } - - return { - a, - b - }; -} +import { Stream } from "@siteimprove/alfa-lang"; +import { Token, TokenType } from "../alphabet"; +import { AnBMicrosyntax } from "../types"; + +export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { + let next = stream.next(); + + if (next === null) { + return null; + } + + let a = 0; + let b = 0; + + switch (next.type) { + case TokenType.Ident: + const oddEven = oddEvenSyntax(next.value); + + if (oddEven !== null) { + return oddEven; + } + + if (next.value !== "n") { + return null; + } + + next = stream.next(); + + break; + case TokenType.Dimension: + if (next.unit !== "n") { + return null; + } + + a = next.value; + + next = stream.next(); + } + + if (next !== null && next.type === TokenType.Number) { + b = next.value; + } else { + stream.backup(1); + } + + return { + a, + b + }; +} + +export function oddEvenSyntax(ident: string): AnBMicrosyntax | null { + switch (ident) { + case "odd": + return { + a: 2, + b: 1 + }; + case "even": + return { + a: 2, + b: 0 + }; + default: + return null; + } +} diff --git a/packages/alfa-css/src/types.ts b/packages/alfa-css/src/types.ts index 23bd4e269a..68054ab02e 100644 --- a/packages/alfa-css/src/types.ts +++ b/packages/alfa-css/src/types.ts @@ -1,359 +1,359 @@ -import { Token } from "./alphabet"; -import { Values } from "./values"; - -/** - * @see https://www.w3.org/TR/css-syntax/#declaration - */ -export interface Declaration { - readonly name: string; - readonly value: Array; - readonly important: boolean; -} - -/** - * @see https://www.w3.org/TR/css-syntax/#at-rule - */ -export interface AtRule { - readonly name: string; - readonly prelude: Array; - readonly value?: Array; -} - -/** - * @see https://www.w3.org/TR/css-syntax/#qualified-rule - */ -export interface QualifiedRule { - readonly prelude: Array; - readonly value: Array; -} - -export type Rule = AtRule | QualifiedRule; - -export const enum SelectorType { - IdSelector = 1, - ClassSelector = 2, - AttributeSelector = 4, - TypeSelector = 8, - PseudoClassSelector = 16, - PseudoElementSelector = 32, - CompoundSelector = 64, - RelativeSelector = 128 -} - -export interface IdSelector { - readonly type: SelectorType.IdSelector; - readonly name: string; -} - -export interface ClassSelector { - readonly type: SelectorType.ClassSelector; - readonly name: string; -} - -export const enum AttributeMatcher { - /** - * @example [foo=bar] - */ - Equal, - - /** - * @example [foo~=bar] - */ - Includes, - - /** - * @example [foo|=bar] - */ - DashMatch, - - /** - * @example [foo^=bar] - */ - Prefix, - - /** - * @example [foo$=bar] - */ - Suffix, - - /** - * @example [foo*=bar] - */ - Substring -} - -export const enum AttributeModifier { - /** - * @example [foo=bar i] - */ - CaseInsensitive = 1 -} - -export interface AttributeSelector { - readonly type: SelectorType.AttributeSelector; - readonly name: string; - readonly namespace: string | null; - readonly value: string | null; - readonly matcher: AttributeMatcher | null; - readonly modifier: number; -} - -export interface TypeSelector { - readonly type: SelectorType.TypeSelector; - readonly name: string; - readonly namespace: string | null; -} - -export interface PseudoClassSelector { - readonly type: SelectorType.PseudoClassSelector; - readonly name: PseudoClass; - readonly value: Selector | Array | AnBMicrosyntax | null; -} - -export interface PseudoElementSelector { - readonly type: SelectorType.PseudoElementSelector; - readonly name: PseudoElement; -} - -export type SimpleSelector = - | IdSelector - | ClassSelector - | TypeSelector - | AttributeSelector - | PseudoClassSelector - | PseudoElementSelector; - -export interface CompoundSelector { - readonly type: SelectorType.CompoundSelector; - readonly left: SimpleSelector; - readonly right: SimpleSelector | CompoundSelector; -} - -export type ComplexSelector = SimpleSelector | CompoundSelector; - -export const enum SelectorCombinator { - /** - * @example div span - */ - Descendant, - - /** - * @example div > span - */ - DirectDescendant, - - /** - * @example div ~ span - */ - Sibling, - - /** - * @example div + span - */ - DirectSibling -} - -export interface RelativeSelector { - readonly type: SelectorType.RelativeSelector; - readonly combinator: SelectorCombinator; - readonly left: ComplexSelector | RelativeSelector; - readonly right: ComplexSelector; -} - -export type Selector = ComplexSelector | RelativeSelector; - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-classes - */ -export type PseudoClass = - // https://www.w3.org/TR/selectors/#matches-pseudo - | "matches" - // https://www.w3.org/TR/selectors/#negation-pseudo - | "not" - // https://www.w3.org/TR/selectors/#something-pseudo - | "something" - // https://www.w3.org/TR/selectors/#has-pseudo - | "has" - // https://www.w3.org/TR/selectors/#dir-pseudo - | "dir" - // https://www.w3.org/TR/selectors/#lang-pseudo - | "lang" - // https://www.w3.org/TR/selectors/#any-link-pseudo - | "any-link" - // https://www.w3.org/TR/selectors/#link-pseudo - | "link" - // https://www.w3.org/TR/selectors/#visited-pseudo - | "visited" - // https://www.w3.org/TR/selectors/#local-link-pseudo - | "local-link" - // https://www.w3.org/TR/selectors/#target-pseudo - | "target" - // https://www.w3.org/TR/selectors/#target-within-pseudo - | "target-within" - // https://www.w3.org/TR/selectors/#scope-pseudo - | "scope" - // https://www.w3.org/TR/selectors/#hover-pseudo - | "hover" - // https://www.w3.org/TR/selectors/#active-pseudo - | "active" - // https://www.w3.org/TR/selectors/#focus-pseudo - | "focus" - // https://www.w3.org/TR/selectors/#focus-visible-pseudo - | "focus-visible" - // https://www.w3.org/TR/selectors/#focus-within-pseudo - | "focus-within" - // https://www.w3.org/TR/selectors/#drag-pseudos - | "drop" - // https://www.w3.org/TR/selectors/#current-pseudo - | "current" - // https://www.w3.org/TR/selectors/#past-pseudo - | "past" - // https://www.w3.org/TR/selectors/#future-pseudo - | "future" - // https://www.w3.org/TR/selectors/#video-state - | "playing" - | "paused" - // https://www.w3.org/TR/selectors/#enabled-pseudo - | "enabled" - // https://www.w3.org/TR/selectors/#disabled-pseudo - | "disabled" - // https://www.w3.org/TR/selectors/#read-only-pseudo - | "read-only" - // https://www.w3.org/TR/selectors/#read-write-pseudo - | "read-write" - // https://www.w3.org/TR/selectors/#placeholder-shown-pseudo - | "placeholder-shown" - // https://www.w3.org/TR/selectors/#default-pseudo - | "default" - // https://www.w3.org/TR/selectors/#checked-pseudo - | "checked" - // https://www.w3.org/TR/selectors/#indetermine-pseudo - | "indetermine" - // https://www.w3.org/TR/selectors/#valid-pseudo - | "valid" - // https://www.w3.org/TR/selectors/#invalid-pseudo - | "invalid" - // https://www.w3.org/TR/selectors/#in-range-pseudo - | "in-range" - // https://www.w3.org/TR/selectors/#out-of-range-pseudo - | "out-of-range" - // https://www.w3.org/TR/selectors/#required-pseudo - | "required" - // https://www.w3.org/TR/selectors/#user-invalid-pseudo - | "user-invalid" - // https://drafts.csswg.org/css-scoping/#host-selector - | "host" - // https://drafts.csswg.org/css-scoping/#host-selector - | "host-context" - // https://www.w3.org/TR/selectors/#root-pseudo - | "root" - // https://www.w3.org/TR/selectors/#empty-pseudo - | "empty" - // https://www.w3.org/TR/selectors/#blank-pseudo - | "blank" - // https://www.w3.org/TR/selectors/#first-child-pseudo - | "first-child" - // https://www.w3.org/TR/selectors/#last-child-pseudo - | "last-child" - // https://www.w3.org/TR/selectors/#only-child-pseudo - | "only-child" - // https://www.w3.org/TR/selectors/#first-of-type-pseudo - | "first-of-type" - // https://www.w3.org/TR/selectors/#last-of-type-pseudo - | "last-of-type" - // https://www.w3.org/TR/selectors/#only-of-type-pseudo - | "only-of-type" - // https://www.w3.org/TR/selectors/#child-index - | ChildIndexedPseudoClass; - -type ChildIndexedPseudoClass = - // https://www.w3.org/TR/selectors/#nth-child-pseudo - | "nth-child" - // https://www.w3.org/TR/selectors/#nth-last-child-pseudo - | "nth-last-child" - // https://www.w3.org/TR/selectors/#nth-of-type-pseudo - | "nth-of-type" - // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo - | "nth-last-of-type" - // https://www.w3.org/TR/selectors/#nth-col-pseudo - | "nth-col" - // https://www.w3.org/TR/selectors/#nth-last-col-pseudo - | "nth-last-col"; - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-elements - */ -export type PseudoElement = - // https://www.w3.org/TR/css-pseudo/#first-line-pseudo - | "first-line" - // https://www.w3.org/TR/css-pseudo/#first-letter-pseudo - | "first-letter" - // https://www.w3.org/TR/css-pseudo/#highlight-pseudos - | "selection" - | "inactive-selection" - | "spelling-error" - | "grammar-error" - // https://www.w3.org/TR/css-pseudo/#generated-content - | "before" - | "after" - // https://www.w3.org/TR/css-pseudo/#marker-pseudo - | "marker" - // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo - | "placeholder"; - -export interface AnBMicrosyntax { - readonly a: number; - readonly b: number; -} - -export const enum MediaQualifier { - Only, - Not -} - -export const enum MediaOperator { - Not, - And, - Or -} - -export const enum MediaComparator { - GreaterThan, - GreaterThanEqual, - LessThan, - LessTahnEqual -} - -export type MediaType = string; - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-query - */ -export interface MediaQuery { - readonly qualifier?: MediaQualifier; - readonly type?: MediaType; - readonly condition?: MediaCondition; -} - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-condition - */ -export interface MediaCondition { - readonly operator?: MediaOperator; - readonly features: Array; -} - -/** - * @see https://www.w3.org/TR/mediaqueries/#typedef-media-feature - */ -export interface MediaFeature { - readonly name: string; - readonly value?: MediaFeatureValue; - readonly comparator?: MediaComparator; -} - -export type MediaFeatureValue = - | Values.Number - | Values.Percentage - | Values.Length - | Values.String; +import { Token } from "./alphabet"; +import { Values } from "./values"; + +/** + * @see https://www.w3.org/TR/css-syntax/#declaration + */ +export interface Declaration { + readonly name: string; + readonly value: Array; + readonly important: boolean; +} + +/** + * @see https://www.w3.org/TR/css-syntax/#at-rule + */ +export interface AtRule { + readonly name: string; + readonly prelude: Array; + readonly value?: Array; +} + +/** + * @see https://www.w3.org/TR/css-syntax/#qualified-rule + */ +export interface QualifiedRule { + readonly prelude: Array; + readonly value: Array; +} + +export type Rule = AtRule | QualifiedRule; + +export const enum SelectorType { + IdSelector = 1, + ClassSelector = 2, + AttributeSelector = 4, + TypeSelector = 8, + PseudoClassSelector = 16, + PseudoElementSelector = 32, + CompoundSelector = 64, + RelativeSelector = 128 +} + +export interface IdSelector { + readonly type: SelectorType.IdSelector; + readonly name: string; +} + +export interface ClassSelector { + readonly type: SelectorType.ClassSelector; + readonly name: string; +} + +export const enum AttributeMatcher { + /** + * @example [foo=bar] + */ + Equal, + + /** + * @example [foo~=bar] + */ + Includes, + + /** + * @example [foo|=bar] + */ + DashMatch, + + /** + * @example [foo^=bar] + */ + Prefix, + + /** + * @example [foo$=bar] + */ + Suffix, + + /** + * @example [foo*=bar] + */ + Substring +} + +export const enum AttributeModifier { + /** + * @example [foo=bar i] + */ + CaseInsensitive = 1 +} + +export interface AttributeSelector { + readonly type: SelectorType.AttributeSelector; + readonly name: string; + readonly namespace: string | null; + readonly value: string | null; + readonly matcher: AttributeMatcher | null; + readonly modifier: number; +} + +export interface TypeSelector { + readonly type: SelectorType.TypeSelector; + readonly name: string; + readonly namespace: string | null; +} + +export interface PseudoClassSelector { + readonly type: SelectorType.PseudoClassSelector; + readonly name: PseudoClass; + readonly value: Selector | Array | AnBMicrosyntax | null; +} + +export interface PseudoElementSelector { + readonly type: SelectorType.PseudoElementSelector; + readonly name: PseudoElement; +} + +export type SimpleSelector = + | IdSelector + | ClassSelector + | TypeSelector + | AttributeSelector + | PseudoClassSelector + | PseudoElementSelector; + +export interface CompoundSelector { + readonly type: SelectorType.CompoundSelector; + readonly left: SimpleSelector; + readonly right: SimpleSelector | CompoundSelector; +} + +export type ComplexSelector = SimpleSelector | CompoundSelector; + +export const enum SelectorCombinator { + /** + * @example div span + */ + Descendant, + + /** + * @example div > span + */ + DirectDescendant, + + /** + * @example div ~ span + */ + Sibling, + + /** + * @example div + span + */ + DirectSibling +} + +export interface RelativeSelector { + readonly type: SelectorType.RelativeSelector; + readonly combinator: SelectorCombinator; + readonly left: ComplexSelector | RelativeSelector; + readonly right: ComplexSelector; +} + +export type Selector = ComplexSelector | RelativeSelector; + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-classes + */ +export type PseudoClass = + // https://www.w3.org/TR/selectors/#matches-pseudo + | "matches" + // https://www.w3.org/TR/selectors/#negation-pseudo + | "not" + // https://www.w3.org/TR/selectors/#something-pseudo + | "something" + // https://www.w3.org/TR/selectors/#has-pseudo + | "has" + // https://www.w3.org/TR/selectors/#dir-pseudo + | "dir" + // https://www.w3.org/TR/selectors/#lang-pseudo + | "lang" + // https://www.w3.org/TR/selectors/#any-link-pseudo + | "any-link" + // https://www.w3.org/TR/selectors/#link-pseudo + | "link" + // https://www.w3.org/TR/selectors/#visited-pseudo + | "visited" + // https://www.w3.org/TR/selectors/#local-link-pseudo + | "local-link" + // https://www.w3.org/TR/selectors/#target-pseudo + | "target" + // https://www.w3.org/TR/selectors/#target-within-pseudo + | "target-within" + // https://www.w3.org/TR/selectors/#scope-pseudo + | "scope" + // https://www.w3.org/TR/selectors/#hover-pseudo + | "hover" + // https://www.w3.org/TR/selectors/#active-pseudo + | "active" + // https://www.w3.org/TR/selectors/#focus-pseudo + | "focus" + // https://www.w3.org/TR/selectors/#focus-visible-pseudo + | "focus-visible" + // https://www.w3.org/TR/selectors/#focus-within-pseudo + | "focus-within" + // https://www.w3.org/TR/selectors/#drag-pseudos + | "drop" + // https://www.w3.org/TR/selectors/#current-pseudo + | "current" + // https://www.w3.org/TR/selectors/#past-pseudo + | "past" + // https://www.w3.org/TR/selectors/#future-pseudo + | "future" + // https://www.w3.org/TR/selectors/#video-state + | "playing" + | "paused" + // https://www.w3.org/TR/selectors/#enabled-pseudo + | "enabled" + // https://www.w3.org/TR/selectors/#disabled-pseudo + | "disabled" + // https://www.w3.org/TR/selectors/#read-only-pseudo + | "read-only" + // https://www.w3.org/TR/selectors/#read-write-pseudo + | "read-write" + // https://www.w3.org/TR/selectors/#placeholder-shown-pseudo + | "placeholder-shown" + // https://www.w3.org/TR/selectors/#default-pseudo + | "default" + // https://www.w3.org/TR/selectors/#checked-pseudo + | "checked" + // https://www.w3.org/TR/selectors/#indetermine-pseudo + | "indetermine" + // https://www.w3.org/TR/selectors/#valid-pseudo + | "valid" + // https://www.w3.org/TR/selectors/#invalid-pseudo + | "invalid" + // https://www.w3.org/TR/selectors/#in-range-pseudo + | "in-range" + // https://www.w3.org/TR/selectors/#out-of-range-pseudo + | "out-of-range" + // https://www.w3.org/TR/selectors/#required-pseudo + | "required" + // https://www.w3.org/TR/selectors/#user-invalid-pseudo + | "user-invalid" + // https://drafts.csswg.org/css-scoping/#host-selector + | "host" + // https://drafts.csswg.org/css-scoping/#host-selector + | "host-context" + // https://www.w3.org/TR/selectors/#root-pseudo + | "root" + // https://www.w3.org/TR/selectors/#empty-pseudo + | "empty" + // https://www.w3.org/TR/selectors/#blank-pseudo + | "blank" + // https://www.w3.org/TR/selectors/#first-child-pseudo + | "first-child" + // https://www.w3.org/TR/selectors/#last-child-pseudo + | "last-child" + // https://www.w3.org/TR/selectors/#only-child-pseudo + | "only-child" + // https://www.w3.org/TR/selectors/#first-of-type-pseudo + | "first-of-type" + // https://www.w3.org/TR/selectors/#last-of-type-pseudo + | "last-of-type" + // https://www.w3.org/TR/selectors/#only-of-type-pseudo + | "only-of-type" + // https://www.w3.org/TR/selectors/#child-index + | ChildIndexedPseudoClass; + +type ChildIndexedPseudoClass = + // https://www.w3.org/TR/selectors/#nth-child-pseudo + | "nth-child" + // https://www.w3.org/TR/selectors/#nth-last-child-pseudo + | "nth-last-child" + // https://www.w3.org/TR/selectors/#nth-of-type-pseudo + | "nth-of-type" + // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo + | "nth-last-of-type" + // https://www.w3.org/TR/selectors/#nth-col-pseudo + | "nth-col" + // https://www.w3.org/TR/selectors/#nth-last-col-pseudo + | "nth-last-col"; + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-elements + */ +export type PseudoElement = + // https://www.w3.org/TR/css-pseudo/#first-line-pseudo + | "first-line" + // https://www.w3.org/TR/css-pseudo/#first-letter-pseudo + | "first-letter" + // https://www.w3.org/TR/css-pseudo/#highlight-pseudos + | "selection" + | "inactive-selection" + | "spelling-error" + | "grammar-error" + // https://www.w3.org/TR/css-pseudo/#generated-content + | "before" + | "after" + // https://www.w3.org/TR/css-pseudo/#marker-pseudo + | "marker" + // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo + | "placeholder"; + +export interface AnBMicrosyntax { + readonly a: number; + readonly b: number; +} + +export const enum MediaQualifier { + Only, + Not +} + +export const enum MediaOperator { + Not, + And, + Or +} + +export const enum MediaComparator { + GreaterThan, + GreaterThanEqual, + LessThan, + LessTahnEqual +} + +export type MediaType = string; + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-query + */ +export interface MediaQuery { + readonly qualifier?: MediaQualifier; + readonly type?: MediaType; + readonly condition?: MediaCondition; +} + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-condition + */ +export interface MediaCondition { + readonly operator?: MediaOperator; + readonly features: Array; +} + +/** + * @see https://www.w3.org/TR/mediaqueries/#typedef-media-feature + */ +export interface MediaFeature { + readonly name: string; + readonly value?: MediaFeatureValue; + readonly comparator?: MediaComparator; +} + +export type MediaFeatureValue = + | Values.Number + | Values.Percentage + | Values.Length + | Values.String; diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 9a894355f9..69a35a276c 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -1,841 +1,915 @@ -import { lex, parse } from "@siteimprove/alfa-lang"; -import { Assertions, test } from "@siteimprove/alfa-test"; -import { Alphabet } from "../../src/alphabet"; -import { SelectorGrammar } from "../../src/grammars/selector"; -import { - AttributeMatcher, - AttributeModifier, - Selector, - SelectorCombinator, - SelectorType -} from "../../src/types"; - -function selector( - t: Assertions, - input: string, - expected: Selector | Array | null -) { - const lexer = lex(input, Alphabet); - const parser = parse(lexer.result, SelectorGrammar); - - t.deepEqual(parser.result, expected, input); -} - -test("Can parse a type selector", t => { - selector(t, "div", { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }); -}); - -test("Can parse an uppercase type selector", t => { - selector(t, "DIV", { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }); -}); - -test("Can parse a type selector with a namespace", t => { - selector(t, "svg|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "svg" - }); -}); - -test("Can parse a type selector with an empty namespace", t => { - selector(t, "|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "" - }); -}); - -test("Can parse the universal selector with an empty namespace", t => { - selector(t, "|*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: "" - }); -}); - -test("Can parse a type selector with the universal namespace", t => { - selector(t, "*|a", { - type: SelectorType.TypeSelector, - name: "a", - namespace: "*" - }); -}); - -test("Can parse the universal selector with the universal namespace", t => { - selector(t, "*|*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: "*" - }); -}); - -test("Can parse a class selector", t => { - selector(t, ".foo", { - type: SelectorType.ClassSelector, - name: "foo" - }); -}); - -test("Can parse an ID selector", t => { - selector(t, "#foo", { - type: SelectorType.IdSelector, - name: "foo" - }); -}); - -test("Can parse a compound selector", t => { - selector(t, "#foo.bar", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.IdSelector, - name: "foo" - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - }); -}); - -test("Can parse the universal selector", t => { - selector(t, "*", { - type: SelectorType.TypeSelector, - name: "*", - namespace: null - }); -}); - -test("Can parse a compound selector with a type in prefix position", t => { - selector(t, "div.foo", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a single descendant selector", t => { - selector(t, "div .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a single descendant selector with a right-hand type selector", t => { - selector(t, "div span", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - } - }); -}); - -test("Can parse a double descendant selector", t => { - selector(t, "div .foo #bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - right: { - type: SelectorType.IdSelector, - name: "bar" - } - }); -}); - -test("Can parse a direct descendant selector", t => { - selector(t, "div > .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a sibling selector", t => { - selector(t, "div ~ .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a direct sibling selector", t => { - selector(t, "div + .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectSibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a list of simple selectors", t => { - selector(t, ".foo, .bar, .baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.ClassSelector, - name: "bar" - }, - { - type: SelectorType.ClassSelector, - name: "baz" - } - ]); -}); - -test("Can parse a list of simple and compound selectors", t => { - selector(t, ".foo, #bar.baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.IdSelector, - name: "bar" - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of descendant selectors", t => { - selector(t, "div .foo, span .baz", [ - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of sibling selectors", t => { - selector(t, "div ~ .foo, span ~ .baz", [ - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - ]); -}); - -test("Can parse a list of selectors with no whitespace", t => { - selector(t, ".foo,.bar,.baz", [ - { - type: SelectorType.ClassSelector, - name: "foo" - }, - { - type: SelectorType.ClassSelector, - name: "bar" - }, - { - type: SelectorType.ClassSelector, - name: "baz" - } - ]); -}); - -test("Can parse a compound selector relative to a class selector", t => { - selector(t, ".foo div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a compound selector relative to a compound selector", t => { - selector(t, "span.foo div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a descendant selector relative to a sibling selector", t => { - selector(t, "div ~ span .foo", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Sibling, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.TypeSelector, - name: "span", - namespace: null - } - }, - right: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse an attribute selector without a value", t => { - selector(t, "[foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with an ident value", t => { - selector(t, "[foo=bar]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a string value", t => { - selector(t, '[foo="bar"]', { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a matcher", t => { - selector(t, "[foo*=bar]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Substring, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a casing modifier", t => { - selector(t, "[foo=bar i]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: "bar", - matcher: AttributeMatcher.Equal, - modifier: AttributeModifier.CaseInsensitive - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[*|foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: "*", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[|foo]", { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: "", - value: null, - matcher: null, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar=baz]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: "baz", - matcher: AttributeMatcher.Equal, - modifier: 0 - }); -}); - -test("Can parse an attribute selector with a namespace", t => { - selector(t, "[foo|bar|=baz]", { - type: SelectorType.AttributeSelector, - name: "bar", - namespace: "foo", - value: "baz", - matcher: AttributeMatcher.DashMatch, - modifier: 0 - }); -}); - -test("Can parse an attribute selector when part of a compound selector", t => { - selector(t, ".foo[foo]", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - }); -}); - -test("Can parse an attribute selector when part of a descendant selector", t => { - selector(t, "div [foo]", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - }); -}); - -test("Can parse an attribute selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div[foo]", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.AttributeSelector, - name: "foo", - namespace: null, - value: null, - matcher: null, - modifier: 0 - } - } - }); -}); - -test("Can parse a pseudo-element selector", t => { - selector(t, "::before", { - type: SelectorType.PseudoElementSelector, - name: "before" - }); -}); - -test("Can parse a pseudo-element selector when part of a compound selector", t => { - selector(t, ".foo::before", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - }); -}); - -test("Can parse a pseudo-element selector when part of a descendant selector", t => { - selector(t, "div ::before", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - }); -}); - -test("Can parse a pseudo-element selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div::before", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoElementSelector, - name: "before" - } - } - }); -}); - -test("Only allows pseudo-element selectors as the last selector", t => { - selector(t, "::foo.foo", null); - selector(t, "::foo+foo", null); -}); - -test("Can parse a named pseudo-class selector", t => { - selector(t, ":hover", { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - }); -}); - -test("Can parse a functional pseudo-class selector", t => { - selector(t, ":not(.foo)", { - type: SelectorType.PseudoClassSelector, - name: "not", - value: { - type: SelectorType.ClassSelector, - name: "foo" - } - }); -}); - -test("Can parse a pseudo-class selector when part of a compound selector", t => { - selector(t, "div:hover", { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - }); -}); - -test("Can parse a pseudo-class selector when part of a compound selector relative to a class selector", t => { - selector(t, ".foo div:hover", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - } - }); -}); - -test("Can parse a compound type, class, and pseudo-class selector relative to a class selector", t => { - selector(t, ".foo div.bar:hover", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.Descendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.ClassSelector, - name: "bar" - }, - right: { - type: SelectorType.PseudoClassSelector, - name: "hover", - value: null - } - } - } - }); -}); - -test("Can parse a simple selector relative to a compound selector", t => { - selector(t, ".foo > div.bar", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - } - }); -}); - -test("Can parse a relative selector relative to a compound selector", t => { - selector(t, ".foo > .bar + div.baz", { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectSibling, - left: { - type: SelectorType.RelativeSelector, - combinator: SelectorCombinator.DirectDescendant, - left: { - type: SelectorType.ClassSelector, - name: "foo" - }, - right: { - type: SelectorType.ClassSelector, - name: "bar" - } - }, - right: { - type: SelectorType.CompoundSelector, - left: { - type: SelectorType.TypeSelector, - name: "div", - namespace: null - }, - right: { - type: SelectorType.ClassSelector, - name: "baz" - } - } - }); -}); - -test("Can parse selector with a An+B odd microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - a: 2, - b: 1 - } - }; - selector(t, ":nth-child(2n+1)", expected); - selector(t, ":nth-child(odd)", expected); - // selector(t, ":nth-child( odd )", expected); // Solve with split() -}); - -test("Can parse selector with a An+B even microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - a: 2, - b: 0 - } - }; - selector(t, ":nth-child(2n+0)", expected); - selector(t, ":nth-child(even)", expected); - // selector(t, ":nth-child( even )", expected); // Solve with split() -}); +import { lex, parse } from "@siteimprove/alfa-lang"; +import { Assertions, test } from "@siteimprove/alfa-test"; +import { Alphabet } from "../../src/alphabet"; +import { SelectorGrammar } from "../../src/grammars/selector"; +import { + AttributeMatcher, + AttributeModifier, + Selector, + SelectorCombinator, + SelectorType +} from "../../src/types"; + +function selector( + t: Assertions, + input: string, + expected: Selector | Array | null +) { + const lexer = lex(input, Alphabet); + const parser = parse(lexer.result, SelectorGrammar); + + t.deepEqual(parser.result, expected, input); +} + +test("Can parse a type selector", t => { + selector(t, "div", { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }); +}); + +test("Can parse an uppercase type selector", t => { + selector(t, "DIV", { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }); +}); + +test("Can parse a type selector with a namespace", t => { + selector(t, "svg|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "svg" + }); +}); + +test("Can parse a type selector with an empty namespace", t => { + selector(t, "|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "" + }); +}); + +test("Can parse the universal selector with an empty namespace", t => { + selector(t, "|*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: "" + }); +}); + +test("Can parse a type selector with the universal namespace", t => { + selector(t, "*|a", { + type: SelectorType.TypeSelector, + name: "a", + namespace: "*" + }); +}); + +test("Can parse the universal selector with the universal namespace", t => { + selector(t, "*|*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: "*" + }); +}); + +test("Can parse a class selector", t => { + selector(t, ".foo", { + type: SelectorType.ClassSelector, + name: "foo" + }); +}); + +test("Can parse an ID selector", t => { + selector(t, "#foo", { + type: SelectorType.IdSelector, + name: "foo" + }); +}); + +test("Can parse a compound selector", t => { + selector(t, "#foo.bar", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.IdSelector, + name: "foo" + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + }); +}); + +test("Can parse the universal selector", t => { + selector(t, "*", { + type: SelectorType.TypeSelector, + name: "*", + namespace: null + }); +}); + +test("Can parse a compound selector with a type in prefix position", t => { + selector(t, "div.foo", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a single descendant selector", t => { + selector(t, "div .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a single descendant selector with a right-hand type selector", t => { + selector(t, "div span", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + } + }); +}); + +test("Can parse a double descendant selector", t => { + selector(t, "div .foo #bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + right: { + type: SelectorType.IdSelector, + name: "bar" + } + }); +}); + +test("Can parse a direct descendant selector", t => { + selector(t, "div > .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a sibling selector", t => { + selector(t, "div ~ .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a direct sibling selector", t => { + selector(t, "div + .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectSibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a list of simple selectors", t => { + selector(t, ".foo, .bar, .baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.ClassSelector, + name: "bar" + }, + { + type: SelectorType.ClassSelector, + name: "baz" + } + ]); +}); + +test("Can parse a list of simple and compound selectors", t => { + selector(t, ".foo, #bar.baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.IdSelector, + name: "bar" + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of descendant selectors", t => { + selector(t, "div .foo, span .baz", [ + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of sibling selectors", t => { + selector(t, "div ~ .foo, span ~ .baz", [ + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + ]); +}); + +test("Can parse a list of selectors with no whitespace", t => { + selector(t, ".foo,.bar,.baz", [ + { + type: SelectorType.ClassSelector, + name: "foo" + }, + { + type: SelectorType.ClassSelector, + name: "bar" + }, + { + type: SelectorType.ClassSelector, + name: "baz" + } + ]); +}); + +test("Can parse a compound selector relative to a class selector", t => { + selector(t, ".foo div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a compound selector relative to a compound selector", t => { + selector(t, "span.foo div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a descendant selector relative to a sibling selector", t => { + selector(t, "div ~ span .foo", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Sibling, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.TypeSelector, + name: "span", + namespace: null + } + }, + right: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse an attribute selector without a value", t => { + selector(t, "[foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with an ident value", t => { + selector(t, "[foo=bar]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a string value", t => { + selector(t, '[foo="bar"]', { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a matcher", t => { + selector(t, "[foo*=bar]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Substring, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a casing modifier", t => { + selector(t, "[foo=bar i]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: "bar", + matcher: AttributeMatcher.Equal, + modifier: AttributeModifier.CaseInsensitive + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[*|foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: "*", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[|foo]", { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: "", + value: null, + matcher: null, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar=baz]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: "baz", + matcher: AttributeMatcher.Equal, + modifier: 0 + }); +}); + +test("Can parse an attribute selector with a namespace", t => { + selector(t, "[foo|bar|=baz]", { + type: SelectorType.AttributeSelector, + name: "bar", + namespace: "foo", + value: "baz", + matcher: AttributeMatcher.DashMatch, + modifier: 0 + }); +}); + +test("Can parse an attribute selector when part of a compound selector", t => { + selector(t, ".foo[foo]", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + }); +}); + +test("Can parse an attribute selector when part of a descendant selector", t => { + selector(t, "div [foo]", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + }); +}); + +test("Can parse an attribute selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div[foo]", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.AttributeSelector, + name: "foo", + namespace: null, + value: null, + matcher: null, + modifier: 0 + } + } + }); +}); + +test("Can parse a pseudo-element selector", t => { + selector(t, "::before", { + type: SelectorType.PseudoElementSelector, + name: "before" + }); +}); + +test("Can parse a pseudo-element selector when part of a compound selector", t => { + selector(t, ".foo::before", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + }); +}); + +test("Can parse a pseudo-element selector when part of a descendant selector", t => { + selector(t, "div ::before", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + }); +}); + +test("Can parse a pseudo-element selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div::before", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoElementSelector, + name: "before" + } + } + }); +}); + +test("Only allows pseudo-element selectors as the last selector", t => { + selector(t, "::foo.foo", null); + selector(t, "::foo+foo", null); +}); + +test("Can parse a named pseudo-class selector", t => { + selector(t, ":hover", { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + }); +}); + +test("Can parse a functional pseudo-class selector", t => { + selector(t, ":not(.foo)", { + type: SelectorType.PseudoClassSelector, + name: "not", + value: { + type: SelectorType.ClassSelector, + name: "foo" + } + }); +}); + +test("Can parse a pseudo-class selector when part of a compound selector", t => { + selector(t, "div:hover", { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + }); +}); + +test("Can parse a pseudo-class selector when part of a compound selector relative to a class selector", t => { + selector(t, ".foo div:hover", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + } + }); +}); + +test("Can parse a compound type, class, and pseudo-class selector relative to a class selector", t => { + selector(t, ".foo div.bar:hover", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.Descendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.ClassSelector, + name: "bar" + }, + right: { + type: SelectorType.PseudoClassSelector, + name: "hover", + value: null + } + } + } + }); +}); + +test("Can parse a simple selector relative to a compound selector", t => { + selector(t, ".foo > div.bar", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + } + }); +}); + +test("Can parse a relative selector relative to a compound selector", t => { + selector(t, ".foo > .bar + div.baz", { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectSibling, + left: { + type: SelectorType.RelativeSelector, + combinator: SelectorCombinator.DirectDescendant, + left: { + type: SelectorType.ClassSelector, + name: "foo" + }, + right: { + type: SelectorType.ClassSelector, + name: "bar" + } + }, + right: { + type: SelectorType.CompoundSelector, + left: { + type: SelectorType.TypeSelector, + name: "div", + namespace: null + }, + right: { + type: SelectorType.ClassSelector, + name: "baz" + } + } + }); +}); + +test("Can parse selector with an An+B odd microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 1 + } + }; + selector(t, ":nth-child(2n+1)", expected); + selector(t, ":nth-child(odd)", expected); + selector(t, ":nth-child(2u+1)", null); +}); + +test("Can parse selector with an An+B even microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 0 + } + }; + selector(t, ":nth-child(2n+0)", expected); + selector(t, ":nth-child(even)", expected); +}); + +test("Can parse selector using only 'An' from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 0 + } + }; + selector(t, ":nth-child(2n)", expected); +}); + +test("Can parse selector using only 'n' from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 0, + b: 0 + } + }; + selector(t, ":nth-child(n)", expected); + // selector(t, ":nth-child(-n-0)", expected); +}); + +test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 0, + b: 2 + } + }; + selector(t, ":nth-child(n+2)", expected); +}); + +test("Can parse selector using only 'B' from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 0, + b: 2 + } + }; + selector(t, ":nth-child(2)", expected); +}); + +// test("Can parse selector with an An+B microsyntax with negative integers", t => { +// const expected: Selector = { +// type: SelectorType.PseudoClassSelector, +// name: "nth-child", +// value: { +// a: -2, +// b: -3 +// } +// }; +// selector(t, ":nth-child(-2n-3)", expected); +// }); + +// test("Can parse selector with an An+B microsyntax with whitespace", t => { +// const expected: Selector = { +// type: SelectorType.PseudoClassSelector, +// name: "nth-child", +// value: { +// a: 2, +// b: 0 +// } +// }; +// selector(t, ":nth-child( 2n + 3 )", expected); +// selector(t, ":nth-child(- n+3)", null); +// selector(t, ":nth-child( even )", expected); // Solve with split() +// }); diff --git a/packages/alfa-dom/src/matches.ts b/packages/alfa-dom/src/matches.ts index 29181d43e3..4c15c17905 100644 --- a/packages/alfa-dom/src/matches.ts +++ b/packages/alfa-dom/src/matches.ts @@ -1,757 +1,761 @@ -import { - AnBMicrosyntax, - AttributeMatcher, - AttributeModifier, - AttributeSelector, - ClassSelector, - CompoundSelector, - IdSelector, - parseSelector, - PseudoClassSelector, - PseudoElementSelector, - RelativeSelector, - Selector, - SelectorCombinator, - SelectorType, - TypeSelector -} from "@siteimprove/alfa-css"; -import { AncestorFilter } from "./ancestor-filter"; -import { contains } from "./contains"; -import { getAttribute } from "./get-attribute"; -import { getAttributeNamespace } from "./get-attribute-namespace"; -import { getClosest } from "./get-closest"; -import { getElementNamespace } from "./get-element-namespace"; -import { getId } from "./get-id"; -import { getParentElement } from "./get-parent-element"; -import { getPreviousElementSibling } from "./get-previous-element-sibling"; -import { isElement, isShadowRoot } from "./guards"; -import { hasClass } from "./has-class"; -import { Element, Namespace, Node } from "./types"; - -const { isArray } = Array; - -export type MatchesOptions = Readonly<{ - composed?: boolean; - flattened?: boolean; - - /** - * @see https://www.w3.org/TR/selectors/#scope-element - * @internal - */ - scope?: Element; - - /** - * @see https://drafts.csswg.org/css-scoping/#tree-context - * @internal - */ - treeContext?: Node; - - /** - * @see https://www.w3.org/TR/selectors/#the-hover-pseudo - * @internal - */ - hover?: Element | boolean; - - /** - * @see https://www.w3.org/TR/selectors/#the-active-pseudo - * @internal - */ - active?: Element | boolean; - - /** - * @see https://www.w3.org/TR/selectors/#the-focus-pseudo - * @internal - */ - focus?: Element | boolean; - - /** - * Whether or not to perform selector matching against pseudo-elements. - * - * @see https://www.w3.org/TR/selectors/#pseudo-elements - * @internal - */ - pseudo?: boolean; - - /** - * Ancestor filter used for fast-rejecting elements during selector matching. - * - * @internal - */ - filter?: AncestorFilter; - - /** - * Declared prefixes mapped to namespace URI. - * - * @see https://www.w3.org/TR/selectors/#type-nmsp - * @internal - */ - namespaces?: Map; -}>; - -/** - * Given an element and a context, check if the element matches the given - * selector within the context. - * - * @see https://www.w3.org/TR/dom41/#dom-element-matches - */ -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options?: MatchesOptions -): boolean; - -/** - * @internal - */ -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options: MatchesOptions, - root: Selector -): boolean; - -export function matches( - element: Element, - context: Node, - selector: string | Selector | Array, - options: MatchesOptions = {}, - root: Selector | null = null -): boolean { - if (typeof selector === "string") { - const parsed = parseSelector(selector); - - if (parsed === null) { - return false; - } - - selector = parsed; - } - - if (isArray(selector)) { - for (let i = 0, n = selector.length; i < n; i++) { - const root = selector[i]; - - if (matches(element, context, root, options, root)) { - return true; - } - } - - return false; - } - - const { namespaces } = options; - - // If selector is not targetting Type or Attribute, then abort if it does not - // match the default namespace. - if ( - selector.type !== SelectorType.TypeSelector && - selector.type !== SelectorType.AttributeSelector && - namespaces !== undefined - ) { - const namespaceDefault = namespaces.get(null); - if ( - namespaceDefault !== undefined && - getElementNamespace(element, context) !== namespaceDefault - ) { - return false; - } - } - - if (root === null) { - root = selector; - } - - switch (selector.type) { - case SelectorType.IdSelector: - return matchesId(element, selector); - - case SelectorType.ClassSelector: - return matchesClass(element, selector); - - case SelectorType.TypeSelector: - return matchesType(element, context, selector, options); - - case SelectorType.AttributeSelector: - return matchesAttribute(element, context, selector, options); - - case SelectorType.CompoundSelector: - return matchesCompound(element, context, selector, options, root); - - case SelectorType.RelativeSelector: - return matchesRelative(element, context, selector, options, root); - - case SelectorType.PseudoClassSelector: - return matchesPseudoClass(element, context, selector, options, root); - - case SelectorType.PseudoElementSelector: - return matchesPseudoElement(element, context, selector, options, root); - } -} - -/** - * @see https://www.w3.org/TR/selectors/#id-selectors - */ -function matchesId(element: Element, selector: IdSelector): boolean { - return getId(element) === selector.name; -} - -/** - * @see https://www.w3.org/TR/selectors/#class-html - */ -function matchesClass(element: Element, selector: ClassSelector): boolean { - return hasClass(element, selector.name); -} - -/** - * @see https://www.w3.org/TR/selectors/#type-selectors - */ -function matchesType( - element: Element, - context: Node, - selector: TypeSelector, - options: MatchesOptions -): boolean { - // https://www.w3.org/TR/selectors/#the-universal-selector - if (selector.name === "*") { - return true; - } - - if (!matchesElementNamespace(element, context, selector, options)) { - return false; - } - - return element.localName === selector.name; -} - -/** - * @see https://www.w3.org/TR/selectors/#type-nmsp - */ -function matchesElementNamespace( - element: Element, - context: Node, - selector: TypeSelector, - options: MatchesOptions -): boolean { - if (selector.namespace === "*") { - return true; - } - - if (options.namespaces === undefined && selector.namespace === null) { - return true; - } - - const elementNamespace = getElementNamespace(element, context); - - if (selector.namespace === "" && elementNamespace === null) { - return true; - } - - if (options.namespaces === undefined) { - return false; - } - - const declaredNamespace = options.namespaces.get(selector.namespace); - - return ( - declaredNamespace === undefined || elementNamespace === declaredNamespace - ); -} - -const whitespace = /\s+/; - -/** - * @see https://www.w3.org/TR/selectors/#attribute-selectors - */ -function matchesAttribute( - element: Element, - context: Node, - selector: AttributeSelector, - options: MatchesOptions -): boolean { - if (!matchesAttributeNamespace(element, context, selector, options)) { - return false; - } - - let value = null; - const attributeOptions = { - lowerCase: (selector.modifier & AttributeModifier.CaseInsensitive) !== 0 - }; - - switch (selector.namespace) { - case null: - case "": - value = getAttribute(element, selector.name, attributeOptions); - break; - case "*": - value = getAttribute( - element, - context, - selector.name, - "*", - attributeOptions - ); - break; - default: - // Abort when no namespace is declared - if (options.namespaces === undefined) { - return false; - } - // Selector namespace must match a declared namespace - const declaredNamespace = options.namespaces.get(selector.namespace); - - if (declaredNamespace === undefined) { - return false; - } - - value = getAttribute( - element, - context, - selector.name, - declaredNamespace, - attributeOptions - ); - } - - if (value === null) { - return false; - } - - if (Array.isArray(value)) { - for (let i = 0, n = value.length; i < n; i++) { - if (matchesValue(value[i], selector)) { - return true; - } - } - - return false; - } - - return matchesValue(value, selector); -} - -function matchesValue(value: string, selector: AttributeSelector): boolean { - if (selector.value === null) { - return true; - } - - switch (selector.matcher) { - case AttributeMatcher.Equal: - return selector.value === value; - - case AttributeMatcher.Prefix: - return value.startsWith(selector.value); - - case AttributeMatcher.Suffix: - return value.endsWith(selector.value); - - case AttributeMatcher.Substring: - return value.includes(selector.value); - - case AttributeMatcher.DashMatch: - return value === selector.value || value.startsWith(`${selector.value}-`); - - case AttributeMatcher.Includes: - const parts = value.split(whitespace); - for (let i = 0, n = parts.length; i < n; i++) { - if (parts[i] === selector.value) { - return true; - } - } - return false; - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#attrnmsp - */ -function matchesAttributeNamespace( - element: Element, - context: Node, - selector: AttributeSelector, - options: MatchesOptions -): boolean { - if (selector.namespace === null || selector.namespace === "*") { - return true; - } - - const { attributes } = element; - let attribute = null; - - for (let i = 0, n = attributes.length; i < n; i++) { - if (attributes[i].localName !== selector.name) { - continue; - } - - attribute = attributes[i]; - break; - } - - if (attribute === null) { - return false; - } - - const attributeNamespace = getAttributeNamespace(attribute, context); - - // Selector "[|att]" should only match attributes with no namespace - if (selector.namespace === "") { - return attributeNamespace === null; - } - - if (options.namespaces === undefined) { - return false; - } - - return attributeNamespace === options.namespaces.get(selector.namespace); -} - -/** - * @see https://www.w3.org/TR/selectors/#compound - */ -function matchesCompound( - element: Element, - context: Node, - selector: CompoundSelector, - options: MatchesOptions, - root: Selector -): boolean { - if (!matches(element, context, selector.left, options, root)) { - return false; - } - - return matches(element, context, selector.right, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#combinators - */ -function matchesRelative( - element: Element, - context: Node, - selector: RelativeSelector, - options: MatchesOptions, - root: Selector -): boolean { - // Before any other work is done, check if the left part of the selector can - // be rejected by the ancestor filter optionally passed to `matches()`. Only - // descendant and direct-descendant selectors can potentially be rejected. - if (options.filter !== undefined) { - switch (selector.combinator) { - case SelectorCombinator.Descendant: - case SelectorCombinator.DirectDescendant: - if (canReject(selector.left, options.filter)) { - return false; - } - - // If the selector cannot be rejected, unset the ancestor filter as it - // no longer applies when we start recursively moving up the tree. - options = { ...options, filter: undefined }; - } - } - - // Otherwise, make sure that the right part of the selector, i.e. the part - // that relates to the current element, matches. - if (!matches(element, context, selector.right, options, root)) { - return false; - } - - // If it does, move on the heavy part of the work: Looking either up the tree - // for a descendant match or looking to the side of the tree for a sibling - // match. - switch (selector.combinator) { - case SelectorCombinator.Descendant: - return matchesDescendant(element, context, selector.left, options, root); - - case SelectorCombinator.DirectDescendant: - return matchesDirectDescendant( - element, - context, - selector.left, - options, - root - ); - - case SelectorCombinator.Sibling: - return matchesSibling(element, context, selector.left, options, root); - - case SelectorCombinator.DirectSibling: - return matchesDirectSibling( - element, - context, - selector.left, - options, - root - ); - } -} - -/** - * @see https://www.w3.org/TR/selectors/#descendant-combinators - */ -function matchesDescendant( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - let parentElement = getParentElement(element, context, options); - - while (parentElement !== null) { - if (matches(parentElement, context, selector, options, root)) { - return true; - } - - parentElement = getParentElement(parentElement, context, options); - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#child-combinators - */ -function matchesDirectDescendant( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - const parentElement = getParentElement(element, context, options); - - if (parentElement === null) { - return false; - } - - return matches(parentElement, context, selector, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#general-sibling-combinators - */ -function matchesSibling( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - let previousElementSibling = getPreviousElementSibling( - element, - context, - options - ); - - while (previousElementSibling !== null) { - if (matches(previousElementSibling, context, selector, options, root)) { - return true; - } - - previousElementSibling = getPreviousElementSibling( - previousElementSibling, - context, - options - ); - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#adjacent-sibling-combinators - */ -function matchesDirectSibling( - element: Element, - context: Node, - selector: Selector, - options: MatchesOptions, - root: Selector -): boolean { - const previousElementSibling = getPreviousElementSibling( - element, - context, - options - ); - - if (previousElementSibling === null) { - return false; - } - - return matches(previousElementSibling, context, selector, options, root); -} - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-classes - */ -function matchesPseudoClass( - element: Element, - context: Node, - selector: PseudoClassSelector, - options: MatchesOptions, - root: Selector -): boolean { - if (selector.value !== null && isAnBMicrosyntax(selector.value)) { - return false; - } - - switch (selector.name) { - // https://www.w3.org/TR/selectors/#scope-pseudo - case "scope": - return options.scope === element; - - // https://drafts.csswg.org/css-scoping/#host-selector - case "host": { - // Do not allow prefix (e.g. "div:host") - if (root !== selector) { - return false; - } - - const { treeContext } = options; - - if (treeContext === undefined || !isShadowRoot(treeContext)) { - return false; - } - - const host = getParentElement(treeContext, context, { composed: true }); - - if (host !== element) { - return false; - } - - // Match host with possible selector argument (e.g. ":host(.foo)") - return ( - selector.value === null || - matches(element, context, selector.value, options, root) - ); - } - - case "host-context": { - // Do not allow prefix (e.g. "div:host") - if (root !== selector) { - return false; - } - - const { treeContext } = options; - const query = selector.value; - - if ( - treeContext === undefined || - !isShadowRoot(treeContext) || - query === null - ) { - return false; - } - - const host = getParentElement(treeContext, context, { composed: true }); - - if (host !== element) { - return false; - } - - const predicate = (node: Node) => - isElement(node) && matches(node, context, query, options, root); - - return getClosest(host, context, predicate) !== null; - } - - // https://www.w3.org/TR/selectors/#negation-pseudo - case "not": - return ( - selector.value === null || - !matches(element, context, selector.value, options, root) - ); - - // https://www.w3.org/TR/selectors/#hover-pseudo - case "hover": - const { hover } = options; - - if (hover === undefined || hover === false) { - return false; - } - - return ( - hover === true || contains(element, context, hover, { composed: true }) - ); - - // https://www.w3.org/TR/selectors/#active-pseudo - case "active": - const { active } = options; - - if (active === undefined || active === false) { - return false; - } - - return ( - active === true || - contains(element, context, active, { composed: true }) - ); - - // https://www.w3.org/TR/selectors/#focus-pseudo - case "focus": - const { focus } = options; - - if (focus === undefined || focus === false) { - return false; - } - - return focus === true || element === focus; - } - - return false; -} - -/** - * @see https://www.w3.org/TR/selectors/#pseudo-elements - */ -function matchesPseudoElement( - element: Element, - context: Node, - selector: PseudoElementSelector, - options: MatchesOptions, - root: Selector -): boolean { - return options.pseudo === true; -} - -/** - * Check if a selector can be rejected based on an ancestor filter. - */ -function canReject(selector: Selector, filter: AncestorFilter): boolean { - switch (selector.type) { - case SelectorType.IdSelector: - case SelectorType.ClassSelector: - case SelectorType.TypeSelector: - return !filter.matches(selector); - - case SelectorType.CompoundSelector: - return ( - canReject(selector.left, filter) || canReject(selector.right, filter) - ); - - case SelectorType.RelativeSelector: - const { combinator } = selector; - if ( - combinator === SelectorCombinator.Descendant || - combinator === SelectorCombinator.DirectDescendant - ) { - return ( - canReject(selector.right, filter) || canReject(selector.left, filter) - ); - } - } - - return false; -} - -/** - * Check if a selector is of interface AnBMicrosyntax. - */ -function isAnBMicrosyntax( - selector: AnBMicrosyntax | Selector | Array -): selector is AnBMicrosyntax { - return (selector).a !== undefined; -} +import { + AnBMicrosyntax, + AttributeMatcher, + AttributeModifier, + AttributeSelector, + ClassSelector, + CompoundSelector, + IdSelector, + parseSelector, + PseudoClassSelector, + PseudoElementSelector, + RelativeSelector, + Selector, + SelectorCombinator, + SelectorType, + TypeSelector +} from "@siteimprove/alfa-css"; +import { AncestorFilter } from "./ancestor-filter"; +import { contains } from "./contains"; +import { getAttribute } from "./get-attribute"; +import { getAttributeNamespace } from "./get-attribute-namespace"; +import { getClosest } from "./get-closest"; +import { getElementNamespace } from "./get-element-namespace"; +import { getId } from "./get-id"; +import { getParentElement } from "./get-parent-element"; +import { getPreviousElementSibling } from "./get-previous-element-sibling"; +import { isElement, isShadowRoot } from "./guards"; +import { hasClass } from "./has-class"; +import { Element, Namespace, Node } from "./types"; + +const { isArray } = Array; + +export type MatchesOptions = Readonly<{ + composed?: boolean; + flattened?: boolean; + + /** + * @see https://www.w3.org/TR/selectors/#scope-element + * @internal + */ + scope?: Element; + + /** + * @see https://drafts.csswg.org/css-scoping/#tree-context + * @internal + */ + treeContext?: Node; + + /** + * @see https://www.w3.org/TR/selectors/#the-hover-pseudo + * @internal + */ + hover?: Element | boolean; + + /** + * @see https://www.w3.org/TR/selectors/#the-active-pseudo + * @internal + */ + active?: Element | boolean; + + /** + * @see https://www.w3.org/TR/selectors/#the-focus-pseudo + * @internal + */ + focus?: Element | boolean; + + /** + * Whether or not to perform selector matching against pseudo-elements. + * + * @see https://www.w3.org/TR/selectors/#pseudo-elements + * @internal + */ + pseudo?: boolean; + + /** + * Ancestor filter used for fast-rejecting elements during selector matching. + * + * @internal + */ + filter?: AncestorFilter; + + /** + * Declared prefixes mapped to namespace URI. + * + * @see https://www.w3.org/TR/selectors/#type-nmsp + * @internal + */ + namespaces?: Map; +}>; + +/** + * Given an element and a context, check if the element matches the given + * selector within the context. + * + * @see https://www.w3.org/TR/dom41/#dom-element-matches + */ +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options?: MatchesOptions +): boolean; + +/** + * @internal + */ +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options: MatchesOptions, + root: Selector +): boolean; + +export function matches( + element: Element, + context: Node, + selector: string | Selector | Array, + options: MatchesOptions = {}, + root: Selector | null = null +): boolean { + if (typeof selector === "string") { + const parsed = parseSelector(selector); + + if (parsed === null) { + return false; + } + + selector = parsed; + } + + if (isArray(selector)) { + for (let i = 0, n = selector.length; i < n; i++) { + const root = selector[i]; + + if (matches(element, context, root, options, root)) { + return true; + } + } + + return false; + } + + const { namespaces } = options; + + // If selector is not targetting Type or Attribute, then abort if it does not + // match the default namespace. + if ( + selector.type !== SelectorType.TypeSelector && + selector.type !== SelectorType.AttributeSelector && + namespaces !== undefined + ) { + const namespaceDefault = namespaces.get(null); + if ( + namespaceDefault !== undefined && + getElementNamespace(element, context) !== namespaceDefault + ) { + return false; + } + } + + if (root === null) { + root = selector; + } + + switch (selector.type) { + case SelectorType.IdSelector: + return matchesId(element, selector); + + case SelectorType.ClassSelector: + return matchesClass(element, selector); + + case SelectorType.TypeSelector: + return matchesType(element, context, selector, options); + + case SelectorType.AttributeSelector: + return matchesAttribute(element, context, selector, options); + + case SelectorType.CompoundSelector: + return matchesCompound(element, context, selector, options, root); + + case SelectorType.RelativeSelector: + return matchesRelative(element, context, selector, options, root); + + case SelectorType.PseudoClassSelector: + return matchesPseudoClass(element, context, selector, options, root); + + case SelectorType.PseudoElementSelector: + return matchesPseudoElement(element, context, selector, options, root); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#id-selectors + */ +function matchesId(element: Element, selector: IdSelector): boolean { + return getId(element) === selector.name; +} + +/** + * @see https://www.w3.org/TR/selectors/#class-html + */ +function matchesClass(element: Element, selector: ClassSelector): boolean { + return hasClass(element, selector.name); +} + +/** + * @see https://www.w3.org/TR/selectors/#type-selectors + */ +function matchesType( + element: Element, + context: Node, + selector: TypeSelector, + options: MatchesOptions +): boolean { + // https://www.w3.org/TR/selectors/#the-universal-selector + if (selector.name === "*") { + return true; + } + + if (!matchesElementNamespace(element, context, selector, options)) { + return false; + } + + return element.localName === selector.name; +} + +/** + * @see https://www.w3.org/TR/selectors/#type-nmsp + */ +function matchesElementNamespace( + element: Element, + context: Node, + selector: TypeSelector, + options: MatchesOptions +): boolean { + if (selector.namespace === "*") { + return true; + } + + if (options.namespaces === undefined && selector.namespace === null) { + return true; + } + + const elementNamespace = getElementNamespace(element, context); + + if (selector.namespace === "" && elementNamespace === null) { + return true; + } + + if (options.namespaces === undefined) { + return false; + } + + const declaredNamespace = options.namespaces.get(selector.namespace); + + return ( + declaredNamespace === undefined || elementNamespace === declaredNamespace + ); +} + +const whitespace = /\s+/; + +/** + * @see https://www.w3.org/TR/selectors/#attribute-selectors + */ +function matchesAttribute( + element: Element, + context: Node, + selector: AttributeSelector, + options: MatchesOptions +): boolean { + if (!matchesAttributeNamespace(element, context, selector, options)) { + return false; + } + + let value = null; + const attributeOptions = { + lowerCase: (selector.modifier & AttributeModifier.CaseInsensitive) !== 0 + }; + + switch (selector.namespace) { + case null: + case "": + value = getAttribute(element, selector.name, attributeOptions); + break; + case "*": + value = getAttribute( + element, + context, + selector.name, + "*", + attributeOptions + ); + break; + default: + // Abort when no namespace is declared + if (options.namespaces === undefined) { + return false; + } + // Selector namespace must match a declared namespace + const declaredNamespace = options.namespaces.get(selector.namespace); + + if (declaredNamespace === undefined) { + return false; + } + + value = getAttribute( + element, + context, + selector.name, + declaredNamespace, + attributeOptions + ); + } + + if (value === null) { + return false; + } + + if (Array.isArray(value)) { + for (let i = 0, n = value.length; i < n; i++) { + if (matchesValue(value[i], selector)) { + return true; + } + } + + return false; + } + + return matchesValue(value, selector); +} + +function matchesValue(value: string, selector: AttributeSelector): boolean { + if (selector.value === null) { + return true; + } + + switch (selector.matcher) { + case AttributeMatcher.Equal: + return selector.value === value; + + case AttributeMatcher.Prefix: + return value.startsWith(selector.value); + + case AttributeMatcher.Suffix: + return value.endsWith(selector.value); + + case AttributeMatcher.Substring: + return value.includes(selector.value); + + case AttributeMatcher.DashMatch: + return value === selector.value || value.startsWith(`${selector.value}-`); + + case AttributeMatcher.Includes: + const parts = value.split(whitespace); + for (let i = 0, n = parts.length; i < n; i++) { + if (parts[i] === selector.value) { + return true; + } + } + return false; + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#attrnmsp + */ +function matchesAttributeNamespace( + element: Element, + context: Node, + selector: AttributeSelector, + options: MatchesOptions +): boolean { + if (selector.namespace === null || selector.namespace === "*") { + return true; + } + + const { attributes } = element; + let attribute = null; + + for (let i = 0, n = attributes.length; i < n; i++) { + if (attributes[i].localName !== selector.name) { + continue; + } + + attribute = attributes[i]; + break; + } + + if (attribute === null) { + return false; + } + + const attributeNamespace = getAttributeNamespace(attribute, context); + + // Selector "[|att]" should only match attributes with no namespace + if (selector.namespace === "") { + return attributeNamespace === null; + } + + if (options.namespaces === undefined) { + return false; + } + + return attributeNamespace === options.namespaces.get(selector.namespace); +} + +/** + * @see https://www.w3.org/TR/selectors/#compound + */ +function matchesCompound( + element: Element, + context: Node, + selector: CompoundSelector, + options: MatchesOptions, + root: Selector +): boolean { + if (!matches(element, context, selector.left, options, root)) { + return false; + } + + return matches(element, context, selector.right, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#combinators + */ +function matchesRelative( + element: Element, + context: Node, + selector: RelativeSelector, + options: MatchesOptions, + root: Selector +): boolean { + // Before any other work is done, check if the left part of the selector can + // be rejected by the ancestor filter optionally passed to `matches()`. Only + // descendant and direct-descendant selectors can potentially be rejected. + if (options.filter !== undefined) { + switch (selector.combinator) { + case SelectorCombinator.Descendant: + case SelectorCombinator.DirectDescendant: + if (canReject(selector.left, options.filter)) { + return false; + } + + // If the selector cannot be rejected, unset the ancestor filter as it + // no longer applies when we start recursively moving up the tree. + options = { ...options, filter: undefined }; + } + } + + // Otherwise, make sure that the right part of the selector, i.e. the part + // that relates to the current element, matches. + if (!matches(element, context, selector.right, options, root)) { + return false; + } + + // If it does, move on the heavy part of the work: Looking either up the tree + // for a descendant match or looking to the side of the tree for a sibling + // match. + switch (selector.combinator) { + case SelectorCombinator.Descendant: + return matchesDescendant(element, context, selector.left, options, root); + + case SelectorCombinator.DirectDescendant: + return matchesDirectDescendant( + element, + context, + selector.left, + options, + root + ); + + case SelectorCombinator.Sibling: + return matchesSibling(element, context, selector.left, options, root); + + case SelectorCombinator.DirectSibling: + return matchesDirectSibling( + element, + context, + selector.left, + options, + root + ); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#descendant-combinators + */ +function matchesDescendant( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + let parentElement = getParentElement(element, context, options); + + while (parentElement !== null) { + if (matches(parentElement, context, selector, options, root)) { + return true; + } + + parentElement = getParentElement(parentElement, context, options); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#child-combinators + */ +function matchesDirectDescendant( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + const parentElement = getParentElement(element, context, options); + + if (parentElement === null) { + return false; + } + + return matches(parentElement, context, selector, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#general-sibling-combinators + */ +function matchesSibling( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + let previousElementSibling = getPreviousElementSibling( + element, + context, + options + ); + + while (previousElementSibling !== null) { + if (matches(previousElementSibling, context, selector, options, root)) { + return true; + } + + previousElementSibling = getPreviousElementSibling( + previousElementSibling, + context, + options + ); + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#adjacent-sibling-combinators + */ +function matchesDirectSibling( + element: Element, + context: Node, + selector: Selector, + options: MatchesOptions, + root: Selector +): boolean { + const previousElementSibling = getPreviousElementSibling( + element, + context, + options + ); + + if (previousElementSibling === null) { + return false; + } + + return matches(previousElementSibling, context, selector, options, root); +} + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-classes + */ +function matchesPseudoClass( + element: Element, + context: Node, + selector: PseudoClassSelector, + options: MatchesOptions, + root: Selector +): boolean { + if (selector.value !== null && isAnBMicrosyntax(selector.value)) { + return false; + } + + switch (selector.name) { + // https://www.w3.org/TR/selectors/#scope-pseudo + case "scope": + return options.scope === element; + + // https://drafts.csswg.org/css-scoping/#host-selector + case "host": { + // Do not allow prefix (e.g. "div:host") + if (root !== selector) { + return false; + } + + const { treeContext } = options; + + if (treeContext === undefined || !isShadowRoot(treeContext)) { + return false; + } + + const host = getParentElement(treeContext, context, { composed: true }); + + if (host !== element) { + return false; + } + + // Match host with possible selector argument (e.g. ":host(.foo)") + return ( + selector.value === null || + matches(element, context, selector.value, options, root) + ); + } + + case "host-context": { + // Do not allow prefix (e.g. "div:host") + if (root !== selector) { + return false; + } + + const { treeContext } = options; + const query = selector.value; + + if ( + treeContext === undefined || + !isShadowRoot(treeContext) || + query === null + ) { + return false; + } + + const host = getParentElement(treeContext, context, { composed: true }); + + if (host !== element) { + return false; + } + + const predicate = (node: Node) => + isElement(node) && matches(node, context, query, options, root); + + return getClosest(host, context, predicate) !== null; + } + + // https://www.w3.org/TR/selectors/#negation-pseudo + case "not": + return ( + selector.value === null || + !matches(element, context, selector.value, options, root) + ); + + // https://www.w3.org/TR/selectors/#hover-pseudo + case "hover": + const { hover } = options; + + if (hover === undefined || hover === false) { + return false; + } + + return ( + hover === true || contains(element, context, hover, { composed: true }) + ); + + // https://www.w3.org/TR/selectors/#active-pseudo + case "active": + const { active } = options; + + if (active === undefined || active === false) { + return false; + } + + return ( + active === true || + contains(element, context, active, { composed: true }) + ); + + // https://www.w3.org/TR/selectors/#focus-pseudo + case "focus": + const { focus } = options; + + if (focus === undefined || focus === false) { + return false; + } + + return focus === true || element === focus; + } + + return false; +} + +/** + * @see https://www.w3.org/TR/selectors/#pseudo-elements + */ +function matchesPseudoElement( + element: Element, + context: Node, + selector: PseudoElementSelector, + options: MatchesOptions, + root: Selector +): boolean { + return options.pseudo === true; +} + +/** + * Check if a selector can be rejected based on an ancestor filter. + */ +function canReject(selector: Selector, filter: AncestorFilter): boolean { + switch (selector.type) { + case SelectorType.IdSelector: + case SelectorType.ClassSelector: + case SelectorType.TypeSelector: + return !filter.matches(selector); + + case SelectorType.CompoundSelector: + return ( + canReject(selector.left, filter) || canReject(selector.right, filter) + ); + + case SelectorType.RelativeSelector: + const { combinator } = selector; + if ( + combinator === SelectorCombinator.Descendant || + combinator === SelectorCombinator.DirectDescendant + ) { + return ( + canReject(selector.right, filter) || canReject(selector.left, filter) + ); + } + } + + return false; +} + +/** + * Check if a selector is of interface AnBMicrosyntax. + */ +function isAnBMicrosyntax( + selector: AnBMicrosyntax | Selector | Array +): selector is AnBMicrosyntax { + return (selector).a !== undefined; +} From 6570dc06374fd9fb3c12c567febcc5abbcc0c311 Mon Sep 17 00:00:00 2001 From: 2biazdk Date: Mon, 11 Feb 2019 14:38:26 +0100 Subject: [PATCH 04/22] Forgot to commit latest work --- .../alfa-css/src/grammars/anb-microsyntax.ts | 99 +++++++++++++------ packages/alfa-css/src/grammars/selector.ts | 2 + packages/alfa-css/test/alphabet.spec.ts | 28 ++++++ .../alfa-css/test/grammars/selector.spec.ts | 72 +++++++------- 4 files changed, 137 insertions(+), 64 deletions(-) diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts index 49d7bfcb1b..38fdc2b615 100644 --- a/packages/alfa-css/src/grammars/anb-microsyntax.ts +++ b/packages/alfa-css/src/grammars/anb-microsyntax.ts @@ -3,30 +3,66 @@ import { Token, TokenType } from "../alphabet"; import { AnBMicrosyntax } from "../types"; export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { - let next = stream.next(); + const next = stream.peek(0); if (next === null) { return null; } - let a = 0; - let b = 0; + console.log(`---`); + console.log(`First: ${JSON.stringify(stream.peek(0))}`); + stream.accept(token => token.type === TokenType.Whitespace); + console.log(`Second: ${JSON.stringify(stream.peek(0))}`); - switch (next.type) { - case TokenType.Ident: - const oddEven = oddEvenSyntax(next.value); + const oddEven = getAnBFromOddEven(stream); - if (oddEven !== null) { - return oddEven; - } + if (oddEven !== null) { + return oddEven; + } - if (next.value !== "n") { - return null; - } + if (next.type === TokenType.Ident) { + return getAnBFromString(stream); + } - next = stream.next(); + return getAnB(stream); +} + +function getAnBFromOddEven(stream: Stream): AnBMicrosyntax | null { + const next = stream.peek(0); + + if (next === null || next.type !== TokenType.Ident) { + return null; + } + + switch (next.value) { + case "odd": + return { + a: 2, + b: 1 + }; + case "even": + return { + a: 2, + b: 0 + }; + default: + return null; + } +} + +function getAnB(stream: Stream): AnBMicrosyntax | null { + let next = stream.peek(0); - break; + if (next === null) { + return null; + } + + let a = 0; + let b = 0; + + console.log(JSON.stringify(next)); + + switch (next.type) { case TokenType.Dimension: if (next.unit !== "n") { return null; @@ -35,8 +71,12 @@ export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { a = next.value; next = stream.next(); + stream.accept(token => token.type === TokenType.Whitespace); } + next = stream.peek(0); + console.log(JSON.stringify(next)); + if (next !== null && next.type === TokenType.Number) { b = next.value; } else { @@ -49,19 +89,22 @@ export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { }; } -export function oddEvenSyntax(ident: string): AnBMicrosyntax | null { - switch (ident) { - case "odd": - return { - a: 2, - b: 1 - }; - case "even": - return { - a: 2, - b: 0 - }; - default: - return null; +function getAnBFromString(stream: Stream): AnBMicrosyntax | null { + const next = stream.peek(0); + + if (next === null || next.type !== TokenType.Ident) { + return null; } + + let a = 0; + const b = 0; + + if (next.value === "n") { + a = 1; + } + + return { + a, + b + }; } diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index aeb53a659f..a44073c37f 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -355,6 +355,8 @@ function pseudoSelector( if (value === null) { return null; } + stream.next(); + stream.accept(token => token.type === TokenType.Whitespace); break; default: value = expression(); diff --git a/packages/alfa-css/test/alphabet.spec.ts b/packages/alfa-css/test/alphabet.spec.ts index 79697d5bf3..b70a5e9456 100644 --- a/packages/alfa-css/test/alphabet.spec.ts +++ b/packages/alfa-css/test/alphabet.spec.ts @@ -346,6 +346,34 @@ test("Can lex an+b values", t => { integer: true } ]); + css(t, "n-4", [ + { + type: TokenType.Ident, + value: "n-4" + } + ]); + css(t, "1n-4", [ + { + type: TokenType.Dimension, + value: 1, + integer: true, + unit: "n-4" + } + ]); + "n-5"; + css(t, "-1n+5", [ + { + type: TokenType.Dimension, + value: -1, + integer: true, + unit: "n" + }, + { + type: TokenType.Number, + value: 5, + integer: true + } + ]); }); test("Can lex an escaped character", t => { diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 69a35a276c..aa925b79f9 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -851,30 +851,30 @@ test("Can parse selector using only 'An' from the An+B microsyntax", t => { selector(t, ":nth-child(2n)", expected); }); -test("Can parse selector using only 'n' from the An+B microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - a: 0, - b: 0 - } - }; - selector(t, ":nth-child(n)", expected); - // selector(t, ":nth-child(-n-0)", expected); -}); +// test("Can parse selector using only 'n' from the An+B microsyntax", t => { +// const expected: Selector = { +// type: SelectorType.PseudoClassSelector, +// name: "nth-child", +// value: { +// a: 1, +// b: 0 +// } +// }; +// selector(t, ":nth-child(n)", expected); +// selector(t, ":nth-child(-n-0)", expected); +// }); -test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { - const expected: Selector = { - type: SelectorType.PseudoClassSelector, - name: "nth-child", - value: { - a: 0, - b: 2 - } - }; - selector(t, ":nth-child(n+2)", expected); -}); +// test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { +// const expected: Selector = { +// type: SelectorType.PseudoClassSelector, +// name: "nth-child", +// value: { +// a: 0, +// b: 2 +// } +// }; +// selector(t, ":nth-child(n+2)", expected); +// }); test("Can parse selector using only 'B' from the An+B microsyntax", t => { const expected: Selector = { @@ -900,16 +900,16 @@ test("Can parse selector using only 'B' from the An+B microsyntax", t => { // selector(t, ":nth-child(-2n-3)", expected); // }); -// test("Can parse selector with an An+B microsyntax with whitespace", t => { -// const expected: Selector = { -// type: SelectorType.PseudoClassSelector, -// name: "nth-child", -// value: { -// a: 2, -// b: 0 -// } -// }; -// selector(t, ":nth-child( 2n + 3 )", expected); -// selector(t, ":nth-child(- n+3)", null); -// selector(t, ":nth-child( even )", expected); // Solve with split() -// }); +test("Can parse selector with an An+B microsyntax with whitespace", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 0 + } + }; + selector(t, ":nth-child( 2n + 3 )", expected); + selector(t, ":nth-child(- n+3)", null); + selector(t, ":nth-child( even )", expected); // Solve with split() +}); From 0fdc60796915edc7f2e17943b3f8ace4a78477ff Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Wed, 26 Jun 2019 10:16:01 +0200 Subject: [PATCH 05/22] WIP on AnB Microsyntax --- .../alfa-css/src/grammars/anb-microsyntax.ts | 51 ++++++++--- .../alfa-css/test/grammars/selector.spec.ts | 86 ++++++++++--------- 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts index 38fdc2b615..4f0823e2f5 100644 --- a/packages/alfa-css/src/grammars/anb-microsyntax.ts +++ b/packages/alfa-css/src/grammars/anb-microsyntax.ts @@ -9,10 +9,10 @@ export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { return null; } - console.log(`---`); - console.log(`First: ${JSON.stringify(stream.peek(0))}`); - stream.accept(token => token.type === TokenType.Whitespace); - console.log(`Second: ${JSON.stringify(stream.peek(0))}`); + stream.accept( + token => + token.type === TokenType.Whitespace || token.type === TokenType.Delim + ); const oddEven = getAnBFromOddEven(stream); @@ -60,22 +60,28 @@ function getAnB(stream: Stream): AnBMicrosyntax | null { let a = 0; let b = 0; - console.log(JSON.stringify(next)); - switch (next.type) { case TokenType.Dimension: - if (next.unit !== "n") { + if (!next.unit.startsWith("n") && !next.unit.startsWith("-")) { return null; } a = next.value; + if (next.unit.length > (next.unit.startsWith("-") ? 2 : 1)) { + b = Number.parseInt( + next.unit.substring(next.unit.startsWith("-") ? 2 : 1) + ); + } + next = stream.next(); - stream.accept(token => token.type === TokenType.Whitespace); + stream.accept( + token => + token.type === TokenType.Whitespace || token.type === TokenType.Delim + ); } next = stream.peek(0); - console.log(JSON.stringify(next)); if (next !== null && next.type === TokenType.Number) { b = next.value; @@ -90,17 +96,38 @@ function getAnB(stream: Stream): AnBMicrosyntax | null { } function getAnBFromString(stream: Stream): AnBMicrosyntax | null { - const next = stream.peek(0); + let next = stream.peek(0); if (next === null || next.type !== TokenType.Ident) { return null; } let a = 0; - const b = 0; + let b = 0; + + if (next.value.startsWith("n")) { + a = 1; + if (next.value.length > 1) { + b = Number.parseInt(next.value.substring(1)); + } + } - if (next.value === "n") { + if (next.value.startsWith("-n")) { a = 1; + if (next.value.length > 2) { + b = Number.parseInt(next.value.substring(2)); + } + } + + next = stream.peek(1); + + if (next !== null && next.type === TokenType.Number) { + stream.advance(1); + b = next.value; + } + + if (b === -0) { + b = 0; } return { diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index aa925b79f9..cf5c246e3e 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -851,30 +851,30 @@ test("Can parse selector using only 'An' from the An+B microsyntax", t => { selector(t, ":nth-child(2n)", expected); }); -// test("Can parse selector using only 'n' from the An+B microsyntax", t => { -// const expected: Selector = { -// type: SelectorType.PseudoClassSelector, -// name: "nth-child", -// value: { -// a: 1, -// b: 0 -// } -// }; -// selector(t, ":nth-child(n)", expected); -// selector(t, ":nth-child(-n-0)", expected); -// }); - -// test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { -// const expected: Selector = { -// type: SelectorType.PseudoClassSelector, -// name: "nth-child", -// value: { -// a: 0, -// b: 2 -// } -// }; -// selector(t, ":nth-child(n+2)", expected); -// }); +test("Can parse selector using only 'n' from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 1, + b: 0 + } + }; + selector(t, ":nth-child(n)", expected); + selector(t, ":nth-child(-n-0)", expected); +}); + +test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 1, + b: 2 + } + }; + selector(t, ":nth-child(n+2)", expected); +}); test("Can parse selector using only 'B' from the An+B microsyntax", t => { const expected: Selector = { @@ -888,28 +888,34 @@ test("Can parse selector using only 'B' from the An+B microsyntax", t => { selector(t, ":nth-child(2)", expected); }); -// test("Can parse selector with an An+B microsyntax with negative integers", t => { -// const expected: Selector = { -// type: SelectorType.PseudoClassSelector, -// name: "nth-child", -// value: { -// a: -2, -// b: -3 -// } -// }; -// selector(t, ":nth-child(-2n-3)", expected); -// }); +test("Can parse selector with an An+B microsyntax with negative integers", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: -2, + b: -3 + } + }; + selector(t, ":nth-child(-2n-3)", expected); +}); test("Can parse selector with an An+B microsyntax with whitespace", t => { - const expected: Selector = { + selector(t, ":nth-child( 2n + 3 )", { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { a: 2, - b: 0 + b: 3 } - }; - selector(t, ":nth-child( 2n + 3 )", expected); + }); selector(t, ":nth-child(- n+3)", null); - selector(t, ":nth-child( even )", expected); // Solve with split() + selector(t, ":nth-child( even )", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: 2, + b: 0 + } + }); // Solve with split() }); From 9cdd40463649c3d1918425f32a8bd3fbe0da53b3 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Wed, 26 Jun 2019 10:32:17 +0200 Subject: [PATCH 06/22] Removing uneeded comment --- packages/alfa-css/test/grammars/selector.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index cf5c246e3e..d4e703b2eb 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -917,5 +917,5 @@ test("Can parse selector with an An+B microsyntax with whitespace", t => { a: 2, b: 0 } - }); // Solve with split() + }); }); From b7a501b0fa6ce63fe8df1fdc68e61d57b942af12 Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Wed, 26 Jun 2019 14:44:25 +0200 Subject: [PATCH 07/22] Scaffold the things! --- .../alfa-css/src/grammars/anb-microsyntax.ts | 137 ------------------ packages/alfa-css/src/grammars/child-index.ts | 52 +++++++ packages/alfa-css/src/types.ts | 4 +- packages/alfa-css/tsconfig.json | 1 + 4 files changed, 55 insertions(+), 139 deletions(-) delete mode 100644 packages/alfa-css/src/grammars/anb-microsyntax.ts create mode 100644 packages/alfa-css/src/grammars/child-index.ts diff --git a/packages/alfa-css/src/grammars/anb-microsyntax.ts b/packages/alfa-css/src/grammars/anb-microsyntax.ts deleted file mode 100644 index 4f0823e2f5..0000000000 --- a/packages/alfa-css/src/grammars/anb-microsyntax.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Stream } from "@siteimprove/alfa-lang"; -import { Token, TokenType } from "../alphabet"; -import { AnBMicrosyntax } from "../types"; - -export function AnBMicrosyntax(stream: Stream): AnBMicrosyntax | null { - const next = stream.peek(0); - - if (next === null) { - return null; - } - - stream.accept( - token => - token.type === TokenType.Whitespace || token.type === TokenType.Delim - ); - - const oddEven = getAnBFromOddEven(stream); - - if (oddEven !== null) { - return oddEven; - } - - if (next.type === TokenType.Ident) { - return getAnBFromString(stream); - } - - return getAnB(stream); -} - -function getAnBFromOddEven(stream: Stream): AnBMicrosyntax | null { - const next = stream.peek(0); - - if (next === null || next.type !== TokenType.Ident) { - return null; - } - - switch (next.value) { - case "odd": - return { - a: 2, - b: 1 - }; - case "even": - return { - a: 2, - b: 0 - }; - default: - return null; - } -} - -function getAnB(stream: Stream): AnBMicrosyntax | null { - let next = stream.peek(0); - - if (next === null) { - return null; - } - - let a = 0; - let b = 0; - - switch (next.type) { - case TokenType.Dimension: - if (!next.unit.startsWith("n") && !next.unit.startsWith("-")) { - return null; - } - - a = next.value; - - if (next.unit.length > (next.unit.startsWith("-") ? 2 : 1)) { - b = Number.parseInt( - next.unit.substring(next.unit.startsWith("-") ? 2 : 1) - ); - } - - next = stream.next(); - stream.accept( - token => - token.type === TokenType.Whitespace || token.type === TokenType.Delim - ); - } - - next = stream.peek(0); - - if (next !== null && next.type === TokenType.Number) { - b = next.value; - } else { - stream.backup(1); - } - - return { - a, - b - }; -} - -function getAnBFromString(stream: Stream): AnBMicrosyntax | null { - let next = stream.peek(0); - - if (next === null || next.type !== TokenType.Ident) { - return null; - } - - let a = 0; - let b = 0; - - if (next.value.startsWith("n")) { - a = 1; - if (next.value.length > 1) { - b = Number.parseInt(next.value.substring(1)); - } - } - - if (next.value.startsWith("-n")) { - a = 1; - if (next.value.length > 2) { - b = Number.parseInt(next.value.substring(2)); - } - } - - next = stream.peek(1); - - if (next !== null && next.type === TokenType.Number) { - stream.advance(1); - b = next.value; - } - - if (b === -0) { - b = 0; - } - - return { - a, - b - }; -} diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts new file mode 100644 index 0000000000..de38274179 --- /dev/null +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -0,0 +1,52 @@ +import * as Lang from "@siteimprove/alfa-lang"; +import { Grammar, Stream } from "@siteimprove/alfa-lang"; +import { Token, Tokens, TokenType } from "../alphabet"; +import { ChildIndex } from "../types"; + +namespace ChildIndex { + export const enum TokenType { + NDimension + } + + export namespace Tokens { + interface Token extends Lang.Token {} + + export interface NDimension extends Token { + // What do we need? + } + } + + export type Token = Tokens.NDimension; +} + +function fromToken(token: Token): ChildIndex.Token | null { + switch (token.type) { + case TokenType.Dimension: + if (token.integer === true && token.unit.toLowerCase() === "n") { + return { + type: ChildIndex.TokenType.NDimension + }; + } + } + + return null; +} + +type Production = Lang.Production; + +const dimension: Production = { + token: TokenType.Dimension +}; + +const ident: Production = { + token: TokenType.Ident +}; + +const number: Production = { + token: TokenType.Number +}; + +export const ChildIndexGrammar: Lang.Grammar< + Token, + ChildIndex +> = new Lang.Grammar([[dimension, ident, number]], () => null); diff --git a/packages/alfa-css/src/types.ts b/packages/alfa-css/src/types.ts index 68054ab02e..e3c715dc71 100644 --- a/packages/alfa-css/src/types.ts +++ b/packages/alfa-css/src/types.ts @@ -107,7 +107,7 @@ export interface TypeSelector { export interface PseudoClassSelector { readonly type: SelectorType.PseudoClassSelector; readonly name: PseudoClass; - readonly value: Selector | Array | AnBMicrosyntax | null; + readonly value: Selector | Array | ChildIndex | null; } export interface PseudoElementSelector { @@ -301,7 +301,7 @@ export type PseudoElement = // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo | "placeholder"; -export interface AnBMicrosyntax { +export interface ChildIndex { readonly a: number; readonly b: number; } diff --git a/packages/alfa-css/tsconfig.json b/packages/alfa-css/tsconfig.json index 9118314ea2..15202dd017 100644 --- a/packages/alfa-css/tsconfig.json +++ b/packages/alfa-css/tsconfig.json @@ -4,6 +4,7 @@ "src/alphabet.ts", "src/converters.ts", "src/escape.ts", + "src/grammars/child-index.ts", "src/grammars/declaration.ts", "src/grammars/media.ts", "src/grammars/rule.ts", From 0bec4081899bb001ff32db510b8c26c5b33f870c Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Wed, 26 Jun 2019 15:49:46 +0200 Subject: [PATCH 08/22] WIP on child index grammar --- packages/alfa-css/src/grammars/child-index.ts | 144 +++++++++++++++++- .../test/grammars/child-index.spec.ts | 57 +++++++ 2 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 packages/alfa-css/test/grammars/child-index.spec.ts diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index de38274179..19fc0b3f48 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -1,32 +1,69 @@ import * as Lang from "@siteimprove/alfa-lang"; -import { Grammar, Stream } from "@siteimprove/alfa-lang"; +// import { Grammar, Stream } from "@siteimprove/alfa-lang"; import { Token, Tokens, TokenType } from "../alphabet"; import { ChildIndex } from "../types"; namespace ChildIndex { export const enum TokenType { - NDimension + NDimension, + NIdent, + NNumber } export namespace Tokens { interface Token extends Lang.Token {} export interface NDimension extends Token { - // What do we need? + readonly unit: string; + readonly value: number; + } + + export interface NIdent extends Token { + readonly value: string; + } + + export interface NNumber extends Token { + readonly value: number; } } - export type Token = Tokens.NDimension; + export type Token = Tokens.NDimension | Tokens.NIdent | Tokens.NNumber; } function fromToken(token: Token): ChildIndex.Token | null { + console.log(token); switch (token.type) { case TokenType.Dimension: if (token.integer === true && token.unit.toLowerCase() === "n") { return { + type: ChildIndex.TokenType.NDimension, + unit: "n", + value: token.value + }; + } else if (token.unit.toLowerCase().startsWith("n")) { + return { + value: token.value, + unit: token.unit.toLowerCase(), + type: ChildIndex.TokenType.NDimension + }; + } else if (token.unit.toLowerCase().startsWith("-n")) { + return { + value: token.value, + unit: token.unit.toLowerCase(), type: ChildIndex.TokenType.NDimension }; } + break; + case TokenType.Ident: + return { + value: token.value, + type: ChildIndex.TokenType.NIdent + }; + case TokenType.Number: + return { + value: token.value, + type: ChildIndex.TokenType.NNumber + }; } return null; @@ -35,15 +72,108 @@ function fromToken(token: Token): ChildIndex.Token | null { type Production = Lang.Production; const dimension: Production = { - token: TokenType.Dimension + token: TokenType.Dimension, + prefix(token, stream) { + let a = 0; + let b = 0; + + const childToken = fromToken(token); + + if ( + childToken === null || + childToken.type !== ChildIndex.TokenType.NDimension + ) { + return null; + } + + a = childToken.value; + + if (childToken.unit !== "n") { + const offset = childToken.unit.startsWith("-") ? 2 : 1; + b = Number.parseInt(childToken.unit.substring(offset)); + } + + return { a, b }; + }, + infix(token, stream, expression, left) { + return null; + } }; const ident: Production = { - token: TokenType.Ident + token: TokenType.Ident, + prefix(token, stream) { + const childToken = fromToken(token); + + if ( + childToken === null || + childToken.type !== ChildIndex.TokenType.NIdent + ) { + return null; + } + + switch (childToken.value) { + case "n": + return { + a: 1, + b: 0 + }; + case "-n": + return { + a: -1, + b: 0 + }; + case "even": + return { + a: 2, + b: 0 + }; + case "odd": + return { + a: 2, + b: 1 + }; + } + + return null; + }, + infix(token, stream, expression, left) { + return null; + } }; const number: Production = { - token: TokenType.Number + token: TokenType.Number, + prefix(token, stream) { + const childToken = fromToken(token); + + if ( + childToken === null || + childToken.type !== ChildIndex.TokenType.NDimension + ) { + return null; + } + + return { + a: childToken.value, + b: 0 + }; + }, + infix(token, stream, expression, left) { + const childToken = fromToken(token); + + if ( + childToken === null || + childToken.type !== ChildIndex.TokenType.NNumber + ) { + return null; + } + + return { + a: left.a, + b: childToken.value + }; + } }; export const ChildIndexGrammar: Lang.Grammar< diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts new file mode 100644 index 0000000000..fc87f9b1e6 --- /dev/null +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -0,0 +1,57 @@ +import { lex, parse } from "@siteimprove/alfa-lang"; +import { Assertions, test } from "@siteimprove/alfa-test"; +import { Alphabet } from "../../src/alphabet"; +import { ChildIndexGrammar } from "../../src/grammars/child-index"; +import { ChildIndex } from "../../src/types"; + +function childIndex(t: Assertions, input: string, expected: ChildIndex | null) { + const lexer = lex(input, Alphabet); + const parser = parse(lexer.result, ChildIndexGrammar); + + t.deepEqual(parser.result, expected, input); +} + +test("Can parse a n-dimension signless-integer child index", t => { + childIndex(t, "2n+3", { + a: 2, + b: 3 + }); +}); + +test("Can parse a n-dimension child index", t => { + childIndex(t, "n", { + a: 1, + b: 0 + }); + + childIndex(t, "-n", { + a: -1, + b: 0 + }); +}); + +test("Can parse a even odd child index", t => { + childIndex(t, "even", { + a: 2, + b: 0 + }); + + childIndex(t, "odd", { + a: 2, + b: 1 + }); +}); + +test("Can parse a ndashdigit-dimension signed-integer child index", t => { + childIndex(t, "-2n-3", { + a: -2, + b: -3 + }); +}); + +test("Can parse a ndashdigit-dimension singless-integer child index", t => { + childIndex(t, "-2n3", { + a: -2, + b: 3 + }); +}); From 643841caf9198770c805c37ebc28633d76f346aa Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Wed, 26 Jun 2019 15:57:46 +0200 Subject: [PATCH 09/22] Refactorz --- packages/alfa-css/src/grammars/child-index.ts | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index 19fc0b3f48..aeb892d492 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -1,5 +1,6 @@ import * as Lang from "@siteimprove/alfa-lang"; -// import { Grammar, Stream } from "@siteimprove/alfa-lang"; +import { Grammar } from "@siteimprove/alfa-lang"; +import * as alphabet from "../alphabet"; import { Token, Tokens, TokenType } from "../alphabet"; import { ChildIndex } from "../types"; @@ -17,21 +18,15 @@ namespace ChildIndex { readonly unit: string; readonly value: number; } - - export interface NIdent extends Token { - readonly value: string; - } - - export interface NNumber extends Token { - readonly value: number; - } } - export type Token = Tokens.NDimension | Tokens.NIdent | Tokens.NNumber; + export type Token = + | alphabet.Tokens.Ident + | alphabet.Tokens.Number + | Tokens.NDimension; } function fromToken(token: Token): ChildIndex.Token | null { - console.log(token); switch (token.type) { case TokenType.Dimension: if (token.integer === true && token.unit.toLowerCase() === "n") { @@ -40,13 +35,17 @@ function fromToken(token: Token): ChildIndex.Token | null { unit: "n", value: token.value }; - } else if (token.unit.toLowerCase().startsWith("n")) { + } + + if (token.unit.toLowerCase().startsWith("n")) { return { value: token.value, unit: token.unit.toLowerCase(), type: ChildIndex.TokenType.NDimension }; - } else if (token.unit.toLowerCase().startsWith("-n")) { + } + + if (token.unit.toLowerCase().startsWith("-n")) { return { value: token.value, unit: token.unit.toLowerCase(), @@ -54,16 +53,10 @@ function fromToken(token: Token): ChildIndex.Token | null { }; } break; + case TokenType.Ident: - return { - value: token.value, - type: ChildIndex.TokenType.NIdent - }; case TokenType.Number: - return { - value: token.value, - type: ChildIndex.TokenType.NNumber - }; + return token; } return null; @@ -105,10 +98,7 @@ const ident: Production = { prefix(token, stream) { const childToken = fromToken(token); - if ( - childToken === null || - childToken.type !== ChildIndex.TokenType.NIdent - ) { + if (childToken === null || childToken.type !== TokenType.Ident) { return null; } @@ -162,10 +152,7 @@ const number: Production = { infix(token, stream, expression, left) { const childToken = fromToken(token); - if ( - childToken === null || - childToken.type !== ChildIndex.TokenType.NNumber - ) { + if (childToken === null || childToken.type !== TokenType.Number) { return null; } @@ -176,7 +163,7 @@ const number: Production = { } }; -export const ChildIndexGrammar: Lang.Grammar< - Token, - ChildIndex -> = new Lang.Grammar([[dimension, ident, number]], () => null); +export const ChildIndexGrammar: Grammar = new Lang.Grammar( + [[dimension, ident, number]], + () => null +); From 4289444565a3477f35fcc536ffd37afe23ea3f78 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 11:32:17 +0200 Subject: [PATCH 10/22] WIP on AnB --- packages/alfa-css/src/grammars/child-index.ts | 205 +++++++++++++----- .../test/grammars/child-index.spec.ts | 40 ++++ 2 files changed, 190 insertions(+), 55 deletions(-) diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index aeb892d492..c275d720ab 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -1,60 +1,121 @@ import * as Lang from "@siteimprove/alfa-lang"; -import { Grammar } from "@siteimprove/alfa-lang"; +import { Grammar, skip } from "@siteimprove/alfa-lang"; import * as alphabet from "../alphabet"; import { Token, Tokens, TokenType } from "../alphabet"; import { ChildIndex } from "../types"; namespace ChildIndex { export const enum TokenType { + Ident, + Number, + DashNDashDigitIdent, + Integer, + NDashDigitDimension, + NDashDigitIdent, + NDashDimension, NDimension, - NIdent, - NNumber + SignedInteger, + SignlessInteger } export namespace Tokens { interface Token extends Lang.Token {} - export interface NDimension extends Token { + export interface DashNDashDigitIdent + extends Token { + readonly value: string; + } + + export interface Integer extends Token { + readonly value: number; + } + + export interface NDashDigitDimension + extends Token { + readonly unit: string; + readonly value: number; + } + + export interface NDashDigitIdent extends Token { + readonly value: string; + } + + export interface NDashDimension extends Token { readonly unit: string; readonly value: number; } + + export interface NDimension extends Token { + readonly value: number; + } + + export interface SignedInteger extends Token { + readonly value: number; + } + + export interface SignlessInteger extends Token { + readonly value: number; + } } export type Token = | alphabet.Tokens.Ident | alphabet.Tokens.Number + | Tokens.DashNDashDigitIdent + | Tokens.NDashDigitDimension + | Tokens.NDashDigitIdent + | Tokens.NDashDimension | Tokens.NDimension; } function fromToken(token: Token): ChildIndex.Token | null { + console.log("Converting ", token); switch (token.type) { case TokenType.Dimension: if (token.integer === true && token.unit.toLowerCase() === "n") { return { type: ChildIndex.TokenType.NDimension, - unit: "n", value: token.value }; } - if (token.unit.toLowerCase().startsWith("n")) { + if ( + token.integer === true && + token.unit.toLowerCase().match("n-[0-9]+") !== null + ) { return { - value: token.value, - unit: token.unit.toLowerCase(), - type: ChildIndex.TokenType.NDimension + unit: token.unit, + type: ChildIndex.TokenType.NDashDigitDimension, + value: token.value }; } - if (token.unit.toLowerCase().startsWith("-n")) { + if (token.integer === true && token.unit.toLowerCase().startsWith("n")) { return { - value: token.value, - unit: token.unit.toLowerCase(), - type: ChildIndex.TokenType.NDimension + unit: token.unit, + type: ChildIndex.TokenType.NDashDimension, + value: token.value }; } break; case TokenType.Ident: + if (token.value.toLowerCase().match("-n-[0-9]+") !== null) { + return { + type: ChildIndex.TokenType.DashNDashDigitIdent, + value: token.value + }; + } + + if (token.value.toLowerCase().match("n-[0-9]+") !== null) { + return { + type: ChildIndex.TokenType.NDashDigitIdent, + value: token.value + }; + } + + return token; + case TokenType.Number: return token; } @@ -72,63 +133,100 @@ const dimension: Production = { const childToken = fromToken(token); - if ( - childToken === null || - childToken.type !== ChildIndex.TokenType.NDimension - ) { + if (childToken === null) { return null; } - a = childToken.value; + switch (childToken.type) { + case ChildIndex.TokenType.NDimension: + a = childToken.value; + break; - if (childToken.unit !== "n") { - const offset = childToken.unit.startsWith("-") ? 2 : 1; - b = Number.parseInt(childToken.unit.substring(offset)); + case ChildIndex.TokenType.NDashDimension: + a = childToken.value; + b = Number.parseInt(childToken.unit.substring(1)); + break; + + case ChildIndex.TokenType.NDashDigitDimension: + a = childToken.value !== null ? childToken.value : 1; + if (childToken.unit.length > 1) { + b = Number.parseInt(childToken.unit.substring(1)); + } } return { a, b }; - }, - infix(token, stream, expression, left) { - return null; } }; const ident: Production = { token: TokenType.Ident, prefix(token, stream) { + let a = 0; + let b = 0; + const childToken = fromToken(token); - if (childToken === null || childToken.type !== TokenType.Ident) { + if (childToken === null) { return null; } - switch (childToken.value) { - case "n": - return { - a: 1, - b: 0 - }; - case "-n": - return { - a: -1, - b: 0 - }; - case "even": - return { - a: 2, - b: 0 - }; - case "odd": - return { - a: 2, - b: 1 - }; + switch (childToken.type) { + case ChildIndex.TokenType.NDashDigitIdent: + a = 1; + b = Number.parseInt(childToken.value.substring(1)); + break; + case ChildIndex.TokenType.DashNDashDigitIdent: + a = -1; + b = Number.parseInt(childToken.value.substring(2)); + break; + case TokenType.Ident: + switch (token.value.toLowerCase()) { + case "n": + a = 1; + b = 0; + break; + case "-n": + a = -1; + b = 0; + break; + case "even": + a = 2; + b = 0; + break; + case "odd": + a = 2; + b = 1; + break; + default: + return null; + } + break; + default: + return null; } - return null; + return { a, b }; }, infix(token, stream, expression, left) { - return null; + let a = 0; + let b = 0; + + const childToken = fromToken(token); + + if (childToken === null) { + return null; + } + + switch (childToken.type) { + case ChildIndex.TokenType.NDashDigitIdent: + a = 0; + b = Number.parseInt(childToken.value.substring(2)); + break; + default: + return null; + } + + return { a, b }; } }; @@ -137,16 +235,13 @@ const number: Production = { prefix(token, stream) { const childToken = fromToken(token); - if ( - childToken === null || - childToken.type !== ChildIndex.TokenType.NDimension - ) { + if (childToken === null || childToken.type !== TokenType.Number) { return null; } return { - a: childToken.value, - b: 0 + a: 0, + b: childToken.value }; }, infix(token, stream, expression, left) { @@ -164,6 +259,6 @@ const number: Production = { }; export const ChildIndexGrammar: Grammar = new Lang.Grammar( - [[dimension, ident, number]], + [skip(TokenType.Whitespace), [dimension, ident, number]], () => null ); diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index fc87f9b1e6..d60c75c218 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -24,10 +24,17 @@ test("Can parse a n-dimension child index", t => { b: 0 }); + childIndex(t, "N", { + a: 1, + b: 0 + }); + childIndex(t, "-n", { a: -1, b: 0 }); + + childIndex(t, "--n", null); }); test("Can parse a even odd child index", t => { @@ -49,9 +56,42 @@ test("Can parse a ndashdigit-dimension signed-integer child index", t => { }); }); +test("Can parse a dashndashdigit-ident signed-integer child index", t => { + childIndex(t, "-n-3", { + a: -1, + b: -3 + }); +}); + +test("Can parse a ndashdigit-ident signed-integer child index", t => { + childIndex(t, "n-3", { + a: 1, + b: -3 + }); +}); + +test("Can parse a n-dimension signed-integer child index", t => { + childIndex(t, "n + 3", { + a: 1, + b: 3 + }); +}); + test("Can parse a ndashdigit-dimension singless-integer child index", t => { childIndex(t, "-2n3", { a: -2, b: 3 }); }); + +test("Can parse a number", t => { + childIndex(t, "7", { + a: 0, + b: 7 + }); + + childIndex(t, "-7", { + a: 0, + b: -7 + }); +}); From ee1c3bd5a405d3034b91668c1b6133027c6031cc Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 12:44:09 +0200 Subject: [PATCH 11/22] AnB almost done --- packages/alfa-css/src/grammars/child-index.ts | 24 +------------------ .../test/grammars/child-index.spec.ts | 12 ++++------ 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index c275d720ab..7a98514643 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -69,7 +69,6 @@ namespace ChildIndex { } function fromToken(token: Token): ChildIndex.Token | null { - console.log("Converting ", token); switch (token.type) { case TokenType.Dimension: if (token.integer === true && token.unit.toLowerCase() === "n") { @@ -205,27 +204,6 @@ const ident: Production = { return null; } - return { a, b }; - }, - infix(token, stream, expression, left) { - let a = 0; - let b = 0; - - const childToken = fromToken(token); - - if (childToken === null) { - return null; - } - - switch (childToken.type) { - case ChildIndex.TokenType.NDashDigitIdent: - a = 0; - b = Number.parseInt(childToken.value.substring(2)); - break; - default: - return null; - } - return { a, b }; } }; @@ -259,6 +237,6 @@ const number: Production = { }; export const ChildIndexGrammar: Grammar = new Lang.Grammar( - [skip(TokenType.Whitespace), [dimension, ident, number]], + [[skip(TokenType.Whitespace), dimension, ident, number]], () => null ); diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index d60c75c218..56bade72aa 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -16,6 +16,11 @@ test("Can parse a n-dimension signless-integer child index", t => { a: 2, b: 3 }); + + childIndex(t, "n +3", { + a: 1, + b: 3 + }); }); test("Can parse a n-dimension child index", t => { @@ -70,13 +75,6 @@ test("Can parse a ndashdigit-ident signed-integer child index", t => { }); }); -test("Can parse a n-dimension signed-integer child index", t => { - childIndex(t, "n + 3", { - a: 1, - b: 3 - }); -}); - test("Can parse a ndashdigit-dimension singless-integer child index", t => { childIndex(t, "-2n3", { a: -2, From 997144ba31683c460af4271694c07303c6d22a42 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 14:11:56 +0200 Subject: [PATCH 12/22] refactor selector --- packages/alfa-css/src/grammars/selector.ts | 12 +++++++++--- packages/alfa-lang/src/parse.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index a44073c37f..bf54bcdce3 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -1,5 +1,11 @@ import * as Lang from "@siteimprove/alfa-lang"; -import { Char, Expression, Grammar, Stream } from "@siteimprove/alfa-lang"; +import { + Char, + Expression, + Grammar, + Stream, + parse +} from "@siteimprove/alfa-lang"; import { Token, Tokens, TokenType } from "../alphabet"; import { AttributeMatcher, @@ -20,7 +26,7 @@ import { SimpleSelector, TypeSelector } from "../types"; -import { AnBMicrosyntax } from "./anb-microsyntax"; +import { ChildIndexGrammar } from "./child-index"; const { isArray } = Array; @@ -351,7 +357,7 @@ function pseudoSelector( case "nth-last-of-type": case "nth-col": case "nth-last-col": - value = AnBMicrosyntax(stream); + value = parse(stream, ChildIndexGrammar).result; if (value === null) { return null; } diff --git a/packages/alfa-lang/src/parse.ts b/packages/alfa-lang/src/parse.ts index 46a3e2798e..0b8bc9b2f2 100644 --- a/packages/alfa-lang/src/parse.ts +++ b/packages/alfa-lang/src/parse.ts @@ -11,13 +11,17 @@ export interface ParseResult { } export function parse( - input: ArrayLike, + input: Stream | ArrayLike, grammar: Grammar, offset?: number ): ParseResult { - const readToken: (i: number) => T = i => input[i]; + const readToken: (i: number) => T = i => + input instanceof Stream ? input.peek(i)! : input[i]; - const stream = new Stream(input.length, readToken, offset); + const stream = + input instanceof Stream + ? input + : new Stream(input.length, readToken, offset); const state = grammar.state(); From fcbf621071313e32ede396b27690a8e315490dc1 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 14:21:01 +0200 Subject: [PATCH 13/22] Fixing failing selector test and getting selector up to speed --- packages/alfa-css/src/grammars/selector.ts | 5 ++--- packages/alfa-css/test/grammars/selector.spec.ts | 16 +++++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index bf54bcdce3..41d94a8173 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -3,8 +3,8 @@ import { Char, Expression, Grammar, - Stream, - parse + parse, + Stream } from "@siteimprove/alfa-lang"; import { Token, Tokens, TokenType } from "../alphabet"; import { @@ -361,7 +361,6 @@ function pseudoSelector( if (value === null) { return null; } - stream.next(); stream.accept(token => token.type === TokenType.Whitespace); break; default: diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index d4e703b2eb..2b9f7ef8fa 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -852,16 +852,22 @@ test("Can parse selector using only 'An' from the An+B microsyntax", t => { }); test("Can parse selector using only 'n' from the An+B microsyntax", t => { - const expected: Selector = { + selector(t, ":nth-child(n)", { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { a: 1, b: 0 } - }; - selector(t, ":nth-child(n)", expected); - selector(t, ":nth-child(-n-0)", expected); + }); + selector(t, ":nth-child(-n-0)", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + a: -1, + b: -0 + } + }); }); test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { @@ -901,7 +907,7 @@ test("Can parse selector with an An+B microsyntax with negative integers", t => }); test("Can parse selector with an An+B microsyntax with whitespace", t => { - selector(t, ":nth-child( 2n + 3 )", { + selector(t, ":nth-child( 2n +3 )", { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { From abbab40cd6a5e2106ab0cd0630e4a1102fe0e9d6 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 14:31:19 +0200 Subject: [PATCH 14/22] Alfa-dom up to speed --- packages/alfa-dom/src/matches.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/alfa-dom/src/matches.ts b/packages/alfa-dom/src/matches.ts index 89ab66fc16..7edc96933e 100644 --- a/packages/alfa-dom/src/matches.ts +++ b/packages/alfa-dom/src/matches.ts @@ -1,8 +1,8 @@ import { - AnBMicrosyntax, AttributeMatcher, AttributeModifier, AttributeSelector, + ChildIndex, ClassSelector, CompoundSelector, IdSelector, @@ -629,7 +629,7 @@ function matchesPseudoClass( options: MatchesOptions, root: Selector ): boolean { - if (selector.value !== null && isAnBMicrosyntax(selector.value)) { + if (selector.value !== null && isChildIndexSyntax(selector.value)) { return false; } @@ -792,8 +792,11 @@ function canReject(selector: Selector, filter: AncestorFilter): boolean { /** * Check if a selector is of interface AnBMicrosyntax. */ -function isAnBMicrosyntax( - selector: AnBMicrosyntax | Selector | Array -): selector is AnBMicrosyntax { - return (selector).a !== undefined; +function isChildIndexSyntax( + selector: ChildIndex | Selector | Array +): selector is ChildIndex { + return ( + (selector).a !== undefined && + (selector).b !== undefined + ); } From 8fa3088f214c98af0633247d9154d0d60169377a Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 14:32:37 +0200 Subject: [PATCH 15/22] Fixing parse --- packages/alfa-lang/src/parse.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/alfa-lang/src/parse.ts b/packages/alfa-lang/src/parse.ts index 0b8bc9b2f2..0c590eb49b 100644 --- a/packages/alfa-lang/src/parse.ts +++ b/packages/alfa-lang/src/parse.ts @@ -15,13 +15,15 @@ export function parse( grammar: Grammar, offset?: number ): ParseResult { - const readToken: (i: number) => T = i => - input instanceof Stream ? input.peek(i)! : input[i]; + let stream: Stream; - const stream = - input instanceof Stream - ? input - : new Stream(input.length, readToken, offset); + if (input instanceof Stream) { + stream = input; + } else { + const readToken: (i: number) => T = i => input[i]; + + stream = new Stream(input.length, readToken, offset); + } const state = grammar.state(); From 15836c62191f9c3c1c8b937a6c545540144549a6 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 14:51:45 +0200 Subject: [PATCH 16/22] AnB fixes --- packages/alfa-css/src/grammars/child-index.ts | 15 +++------------ .../alfa-css/test/grammars/child-index.spec.ts | 7 +++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index 7a98514643..f12885f983 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -26,10 +26,6 @@ namespace ChildIndex { readonly value: string; } - export interface Integer extends Token { - readonly value: number; - } - export interface NDashDigitDimension extends Token { readonly unit: string; @@ -48,14 +44,6 @@ namespace ChildIndex { export interface NDimension extends Token { readonly value: number; } - - export interface SignedInteger extends Token { - readonly value: number; - } - - export interface SignlessInteger extends Token { - readonly value: number; - } } export type Token = @@ -116,6 +104,9 @@ function fromToken(token: Token): ChildIndex.Token | null { return token; case TokenType.Number: + if (!token.integer) { + return null; + } return token; } diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index 56bade72aa..56f4c22a5a 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -54,6 +54,13 @@ test("Can parse a even odd child index", t => { }); }); +test("Cannot parse a float index", t => { + childIndex(t, "3.14", null); + childIndex(t, "2n3.14", null); + childIndex(t, "3,14", null); + childIndex(t, "2n3,14", null); +}); + test("Can parse a ndashdigit-dimension signed-integer child index", t => { childIndex(t, "-2n-3", { a: -2, From 7bd774f78b01f330caab55cfd4419ea7e4d51e2a Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 15:01:38 +0200 Subject: [PATCH 17/22] Asserting on floats in number --- packages/alfa-css/src/grammars/selector.ts | 2 ++ packages/alfa-css/test/grammars/child-index.spec.ts | 7 ------- packages/alfa-css/test/grammars/selector.spec.ts | 7 +++++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index 41d94a8173..8c4cd6ceb2 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -358,9 +358,11 @@ function pseudoSelector( case "nth-col": case "nth-last-col": value = parse(stream, ChildIndexGrammar).result; + if (value === null) { return null; } + stream.accept(token => token.type === TokenType.Whitespace); break; default: diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index 56f4c22a5a..56bade72aa 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -54,13 +54,6 @@ test("Can parse a even odd child index", t => { }); }); -test("Cannot parse a float index", t => { - childIndex(t, "3.14", null); - childIndex(t, "2n3.14", null); - childIndex(t, "3,14", null); - childIndex(t, "2n3,14", null); -}); - test("Can parse a ndashdigit-dimension signed-integer child index", t => { childIndex(t, "-2n-3", { a: -2, diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 2b9f7ef8fa..b2311ec9af 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -925,3 +925,10 @@ test("Can parse selector with an An+B microsyntax with whitespace", t => { } }); }); + +test("Cannot parse a float child index", t => { + selector(t, ":nth-child(3.14)", null); + selector(t, ":nth-child(2n3.14)", null); + selector(t, ":nth-child(3,14)", null); + selector(t, ":nth-child(2n3,14)", null); +}); From e32a29835594392209af436878b83ee6d3499e63 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 15:19:11 +0200 Subject: [PATCH 18/22] Adding some more tests --- .../test/grammars/child-index.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index 56bade72aa..be539cbcd3 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -54,6 +54,25 @@ test("Can parse a even odd child index", t => { }); }); +test("Cannot parse a float", t => { + childIndex(t, "3.14", null); +}); + +test("Cannot parse a dimension with wrong unit", t => { + childIndex(t, "3px7", null); +}); + +test("Cannot parse a unknown ident", t => { + childIndex(t, "p", null); +}); + +test("Can parse a n-dash without digit", t => { + childIndex(t, "n -", { + a: 1, + b: 0 + }); +}); + test("Can parse a ndashdigit-dimension signed-integer child index", t => { childIndex(t, "-2n-3", { a: -2, From 2173210d15ffd4697f370b617aaf8182d5dcf6a0 Mon Sep 17 00:00:00 2001 From: Niclas Hedam Date: Thu, 27 Jun 2019 15:19:48 +0200 Subject: [PATCH 19/22] Formatting --- packages/alfa-css/test/grammars/child-index.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index be539cbcd3..c7628a09c9 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -66,13 +66,6 @@ test("Cannot parse a unknown ident", t => { childIndex(t, "p", null); }); -test("Can parse a n-dash without digit", t => { - childIndex(t, "n -", { - a: 1, - b: 0 - }); -}); - test("Can parse a ndashdigit-dimension signed-integer child index", t => { childIndex(t, "-2n-3", { a: -2, From 989a6db0df83fd92e39c70a9f924ca569a6e6dab Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Wed, 7 Aug 2019 11:44:26 +0200 Subject: [PATCH 20/22] Rework child index implementation --- packages/alfa-css/src/alphabet.ts | 9 +- packages/alfa-css/src/grammars/child-index.ts | 352 +++++++++--------- packages/alfa-css/src/grammars/selector.ts | 142 +++---- packages/alfa-css/src/types.ts | 330 +++++++++++----- packages/alfa-css/test/alphabet.spec.ts | 52 ++- .../test/grammars/child-index.spec.ts | 316 +++++++++++++--- .../test/grammars/declaration.spec.ts | 1 + .../alfa-css/test/grammars/selector.spec.ts | 50 +-- packages/alfa-dom/src/matches.ts | 19 +- packages/alfa-lang/src/parse.ts | 10 +- 10 files changed, 836 insertions(+), 445 deletions(-) diff --git a/packages/alfa-css/src/alphabet.ts b/packages/alfa-css/src/alphabet.ts index 475ba568e3..16270dcbfd 100644 --- a/packages/alfa-css/src/alphabet.ts +++ b/packages/alfa-css/src/alphabet.ts @@ -79,6 +79,7 @@ export namespace Tokens { export interface Number extends Token { readonly value: number; readonly integer: boolean; + readonly signed: boolean; } export interface Percentage extends Token { @@ -89,6 +90,7 @@ export namespace Tokens { export interface Dimension extends Token { readonly value: number; readonly integer: boolean; + readonly signed: boolean; readonly unit: string; } @@ -461,10 +463,13 @@ function consumeNumber(char: number, stream: Stream): Tokens.Number { let next: number | null = char; + let isSigned = false; + if (next === Char.PlusSign || next === Char.HyphenMinus) { result.push(next); stream.advance(1); next = stream.peek(0); + isSigned = true; } while (next !== null && isNumeric(next)) { @@ -521,7 +526,8 @@ function consumeNumber(char: number, stream: Stream): Tokens.Number { return { type: TokenType.Number, value: consumeInteger(result), - integer: isInteger + integer: isInteger, + signed: isSigned }; } @@ -541,6 +547,7 @@ function consumeNumeric( type: TokenType.Dimension, value: number.value, integer: number.integer, + signed: number.signed, unit: consumeName(next, stream) }; } diff --git a/packages/alfa-css/src/grammars/child-index.ts b/packages/alfa-css/src/grammars/child-index.ts index f12885f983..131ff3e824 100644 --- a/packages/alfa-css/src/grammars/child-index.ts +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -1,233 +1,229 @@ import * as Lang from "@siteimprove/alfa-lang"; -import { Grammar, skip } from "@siteimprove/alfa-lang"; -import * as alphabet from "../alphabet"; +import { Char, Grammar, isNumeric, skip, Stream } from "@siteimprove/alfa-lang"; import { Token, Tokens, TokenType } from "../alphabet"; import { ChildIndex } from "../types"; -namespace ChildIndex { - export const enum TokenType { - Ident, - Number, - DashNDashDigitIdent, - Integer, - NDashDigitDimension, - NDashDigitIdent, - NDashDimension, - NDimension, - SignedInteger, - SignlessInteger - } - - export namespace Tokens { - interface Token extends Lang.Token {} +type Production = Lang.Production; - export interface DashNDashDigitIdent - extends Token { - readonly value: string; +const number: Production = { + token: TokenType.Number, + prefix(token, stream) { + if (!token.integer) { + return null; } - export interface NDashDigitDimension - extends Token { - readonly unit: string; - readonly value: number; - } + return { + step: 0, + offset: token.value + }; + } +}; - export interface NDashDigitIdent extends Token { - readonly value: string; +const dimension: Production = { + token: TokenType.Dimension, + prefix(token, stream) { + if (!token.integer) { + return null; } - export interface NDashDimension extends Token { - readonly unit: string; - readonly value: number; - } + const unit = token.unit.toLowerCase(); + + switch (unit) { + case "n": + return { step: token.value, offset: parseOffset(stream) }; - export interface NDimension extends Token { - readonly value: number; + case "n-": + return { step: token.value, offset: -1 * parseOffset(stream, true) }; } - } - export type Token = - | alphabet.Tokens.Ident - | alphabet.Tokens.Number - | Tokens.DashNDashDigitIdent - | Tokens.NDashDigitDimension - | Tokens.NDashDigitIdent - | Tokens.NDashDimension - | Tokens.NDimension; -} + { + const stream = new Stream(unit.length, i => unit.charCodeAt(i)); -function fromToken(token: Token): ChildIndex.Token | null { - switch (token.type) { - case TokenType.Dimension: - if (token.integer === true && token.unit.toLowerCase() === "n") { - return { - type: ChildIndex.TokenType.NDimension, - value: token.value - }; - } + let next = stream.peek(0); - if ( - token.integer === true && - token.unit.toLowerCase().match("n-[0-9]+") !== null - ) { - return { - unit: token.unit, - type: ChildIndex.TokenType.NDashDigitDimension, - value: token.value - }; + if (next === null || next !== Char.SmallLetterN) { + return null; } - if (token.integer === true && token.unit.toLowerCase().startsWith("n")) { - return { - unit: token.unit, - type: ChildIndex.TokenType.NDashDimension, - value: token.value - }; - } - break; - - case TokenType.Ident: - if (token.value.toLowerCase().match("-n-[0-9]+") !== null) { - return { - type: ChildIndex.TokenType.DashNDashDigitIdent, - value: token.value - }; - } + stream.advance(1); + next = stream.peek(0); - if (token.value.toLowerCase().match("n-[0-9]+") !== null) { - return { - type: ChildIndex.TokenType.NDashDigitIdent, - value: token.value - }; + if (next === null || next !== Char.HyphenMinus) { + return null; } - return token; + stream.advance(1); + next = stream.peek(0); - case TokenType.Number: - if (!token.integer) { + if (next === null || !isNumeric(next)) { return null; } - return token; - } - return null; -} + let offset = 0; -type Production = Lang.Production; + while (next !== null && isNumeric(next)) { + offset = offset * 10 + next - Char.DigitZero; -const dimension: Production = { - token: TokenType.Dimension, - prefix(token, stream) { - let a = 0; - let b = 0; - - const childToken = fromToken(token); - - if (childToken === null) { - return null; - } - - switch (childToken.type) { - case ChildIndex.TokenType.NDimension: - a = childToken.value; - break; + stream.advance(1); + next = stream.peek(0); + } - case ChildIndex.TokenType.NDashDimension: - a = childToken.value; - b = Number.parseInt(childToken.unit.substring(1)); - break; + if (!stream.done()) { + return null; + } - case ChildIndex.TokenType.NDashDigitDimension: - a = childToken.value !== null ? childToken.value : 1; - if (childToken.unit.length > 1) { - b = Number.parseInt(childToken.unit.substring(1)); - } + return { step: token.value, offset: -1 * offset }; } - return { a, b }; + return null; } }; const ident: Production = { token: TokenType.Ident, prefix(token, stream) { - let a = 0; - let b = 0; + const value = token.value.toLowerCase(); - const childToken = fromToken(token); + switch (value) { + case "n": + return { step: 1, offset: parseOffset(stream) }; - if (childToken === null) { - return null; + case "-n": + return { step: -1, offset: parseOffset(stream) }; + + case "n-": + return { step: 1, offset: -1 * parseOffset(stream, true) }; + + case "-n-": + return { step: -1, offset: -1 * parseOffset(stream, true) }; + + case "even": + return { step: 2, offset: 0 }; + + case "odd": + return { step: 2, offset: 1 }; } - switch (childToken.type) { - case ChildIndex.TokenType.NDashDigitIdent: - a = 1; - b = Number.parseInt(childToken.value.substring(1)); - break; - case ChildIndex.TokenType.DashNDashDigitIdent: - a = -1; - b = Number.parseInt(childToken.value.substring(2)); - break; - case TokenType.Ident: - switch (token.value.toLowerCase()) { - case "n": - a = 1; - b = 0; - break; - case "-n": - a = -1; - b = 0; - break; - case "even": - a = 2; - b = 0; - break; - case "odd": - a = 2; - b = 1; - break; - default: - return null; - } - break; - default: + { + const stream = new Stream(value.length, i => value.charCodeAt(i)); + + let step = 1; + let next = stream.peek(0); + + if (next !== null && next === Char.HyphenMinus) { + step = -1; + + stream.advance(1); + next = stream.peek(0); + } + + if (next === null || next !== Char.SmallLetterN) { return null; - } + } - return { a, b }; - } -}; + stream.advance(1); + next = stream.peek(0); -const number: Production = { - token: TokenType.Number, - prefix(token, stream) { - const childToken = fromToken(token); + if (next === null || next !== Char.HyphenMinus) { + return null; + } - if (childToken === null || childToken.type !== TokenType.Number) { - return null; + stream.advance(1); + next = stream.peek(0); + + if (next === null || !isNumeric(next)) { + return null; + } + + let offset = 0; + + while (next !== null && isNumeric(next)) { + offset = offset * 10 + next - Char.DigitZero; + + stream.advance(1); + next = stream.peek(0); + } + + if (!stream.done()) { + return null; + } + + return { step, offset: -1 * offset }; } + } +}; - return { - a: 0, - b: childToken.value - }; - }, - infix(token, stream, expression, left) { - const childToken = fromToken(token); +const delim: Production = { + token: TokenType.Delim, + prefix(token, stream, expression) { + switch (token.value) { + case Char.PlusSign: { + const next = stream.peek(0); - if (childToken === null || childToken.type !== TokenType.Number) { - return null; + if (next === null || next.type !== TokenType.Ident) { + break; + } + + return expression(); + } } - return { - a: left.a, - b: childToken.value - }; + return null; } }; export const ChildIndexGrammar: Grammar = new Lang.Grammar( - [[skip(TokenType.Whitespace), dimension, ident, number]], + [[skip(TokenType.Whitespace), dimension, ident, number, delim]], () => null ); + +function parseOffset(stream: Stream, signless = false): number { + stream.accept(token => token.type === TokenType.Whitespace); + + let next = stream.peek(0); + + if (next === null) { + return 0; + } + + let sign: number | null = null; + + if (next.type === TokenType.Delim) { + if (signless) { + return 0; + } + + switch (next.value) { + case Char.PlusSign: + sign = 1; + break; + case Char.HyphenMinus: + sign = -1; + break; + default: + return 0; + } + + stream.advance(1); + stream.accept(token => token.type === TokenType.Whitespace); + + signless = true; + next = stream.peek(0); + + if (next === null) { + return 0; + } + } + + if ( + next.type !== TokenType.Number || + !next.integer || + signless === next.signed + ) { + return 0; + } + + stream.advance(1); + + return sign === null ? next.value : sign * next.value; +} diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index 8c4cd6ceb2..da643ba90e 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -15,9 +15,7 @@ import { ComplexSelector, CompoundSelector, IdSelector, - PseudoClass, PseudoClassSelector, - PseudoElement, PseudoElementSelector, RelativeSelector, Selector, @@ -241,8 +239,6 @@ function pseudoSelector( stream: Stream, expression: Expression> ): PseudoElementSelector | PseudoClassSelector | null { - let selector: PseudoElementSelector | PseudoClassSelector; - let next = stream.next(); if (next === null) { @@ -256,8 +252,6 @@ function pseudoSelector( return null; } - let name: PseudoElement; - switch (next.value) { case "first-line": case "first-letter": @@ -269,27 +263,85 @@ function pseudoSelector( case "after": case "marker": case "placeholder": - name = next.value; - break; + return { + type: SelectorType.PseudoElementSelector, + name + }; + default: return null; } + } - selector = { type: SelectorType.PseudoElementSelector, name }; - } else { - if (next.type !== TokenType.Ident && next.type !== TokenType.FunctionName) { + if (next.type === TokenType.Ident) { + return pseudoClassSelector(stream, next.value, false); + } + + if (next.type === TokenType.FunctionName) { + const selector = pseudoClassSelector(stream, next.value, true); + + stream.accept(token => token.type === TokenType.Whitespace); + + next = stream.next(); + + if (next === null || next.type !== TokenType.RightParenthesis) { return null; } - let name: PseudoClass; + return selector; + } + + return null; +} - switch (next.value) { - case "matches": +function pseudoClassSelector( + stream: Stream, + name: string, + functional: boolean +): PseudoClassSelector | null { + if (functional) { + switch (name) { + case "is": case "not": - case "something": case "has": - case "dir": - case "lang": + case "current": + case "host": + case "host-context": { + const value = parse(stream, SelectorGrammar).result; + + if (value === null) { + return null; + } + + return { + type: SelectorType.PseudoClassSelector, + name, + value: isArray(value) ? value : [value] + }; + } + + case "nth-child": + case "nth-last-child": + case "nth-of-type": + case "nth-last-of-type": + case "nth-col": + case "nth-last-col": { + const value = parse(stream, ChildIndexGrammar).result; + + if (value === null) { + return null; + } + + return { + type: SelectorType.PseudoClassSelector, + name, + value + }; + } + } + } else { + // https://github.com/microsoft/TypeScript/issues/32749 + switch (name) { case "any-link": case "link": case "visited": @@ -302,7 +354,6 @@ function pseudoSelector( case "focus": case "focus-visible": case "focus-within": - case "drop": case "current": case "past": case "future": @@ -316,6 +367,11 @@ function pseudoSelector( case "default": case "checked": case "indetermine": + return { + type: SelectorType.PseudoClassSelector, + name + }; + case "valid": case "invalid": case "in-range": @@ -323,7 +379,6 @@ function pseudoSelector( case "required": case "user-invalid": case "host": - case "host-context": case "root": case "empty": case "blank": @@ -333,53 +388,14 @@ function pseudoSelector( case "first-of-type": case "last-of-type": case "only-of-type": - case "nth-child": - case "nth-last-child": - case "nth-of-type": - case "nth-last-of-type": - case "nth-col": - case "nth-last-col": - name = next.value; - break; - default: - return null; - } - - if (next.type === TokenType.Ident) { - selector = { type: SelectorType.PseudoClassSelector, name, value: null }; - } else { - let value = null; - - switch (name) { - case "nth-child": - case "nth-last-child": - case "nth-of-type": - case "nth-last-of-type": - case "nth-col": - case "nth-last-col": - value = parse(stream, ChildIndexGrammar).result; - - if (value === null) { - return null; - } - - stream.accept(token => token.type === TokenType.Whitespace); - break; - default: - value = expression(); - } - - next = stream.next(); - - if (next === null || next.type !== TokenType.RightParenthesis) { - return null; - } - - selector = { type: SelectorType.PseudoClassSelector, name, value }; + return { + type: SelectorType.PseudoClassSelector, + name + }; } } - return selector; + return null; } function compoundSelector( diff --git a/packages/alfa-css/src/types.ts b/packages/alfa-css/src/types.ts index e3c715dc71..c6f5e5c0cd 100644 --- a/packages/alfa-css/src/types.ts +++ b/packages/alfa-css/src/types.ts @@ -104,17 +104,6 @@ export interface TypeSelector { readonly namespace: string | null; } -export interface PseudoClassSelector { - readonly type: SelectorType.PseudoClassSelector; - readonly name: PseudoClass; - readonly value: Selector | Array | ChildIndex | null; -} - -export interface PseudoElementSelector { - readonly type: SelectorType.PseudoElementSelector; - readonly name: PseudoElement; -} - export type SimpleSelector = | IdSelector | ClassSelector @@ -165,145 +154,316 @@ export type Selector = ComplexSelector | RelativeSelector; /** * @see https://www.w3.org/TR/selectors/#pseudo-classes */ -export type PseudoClass = +export namespace PseudoClassSelector { + interface PseudoClassSelector { + readonly type: SelectorType.PseudoClassSelector; + readonly name: N; + } + + export interface WithValue { + readonly value: V; + } + + export interface WithSelector extends WithValue> {} + + export interface WithChildIndex extends WithValue {} + // https://www.w3.org/TR/selectors/#matches-pseudo - | "matches" + export interface Is extends PseudoClassSelector<"is">, WithSelector {} + // https://www.w3.org/TR/selectors/#negation-pseudo - | "not" - // https://www.w3.org/TR/selectors/#something-pseudo - | "something" + export interface Not extends PseudoClassSelector<"not">, WithSelector {} + // https://www.w3.org/TR/selectors/#has-pseudo - | "has" + export interface Has extends PseudoClassSelector<"has">, WithSelector {} + // https://www.w3.org/TR/selectors/#dir-pseudo - | "dir" + export interface Dir + extends PseudoClassSelector<"dir">, + WithValue<"ltr" | "rtl"> {} + // https://www.w3.org/TR/selectors/#lang-pseudo - | "lang" + export interface Lang + extends PseudoClassSelector<"lang">, + WithValue> {} + // https://www.w3.org/TR/selectors/#any-link-pseudo - | "any-link" + export interface AnyLink extends PseudoClassSelector<"any-link"> {} + // https://www.w3.org/TR/selectors/#link-pseudo - | "link" + export interface Link extends PseudoClassSelector<"link"> {} + // https://www.w3.org/TR/selectors/#visited-pseudo - | "visited" + export interface Visited extends PseudoClassSelector<"visited"> {} + // https://www.w3.org/TR/selectors/#local-link-pseudo - | "local-link" + export interface LocalLink extends PseudoClassSelector<"local-link"> {} + // https://www.w3.org/TR/selectors/#target-pseudo - | "target" + export interface Target extends PseudoClassSelector<"target"> {} + // https://www.w3.org/TR/selectors/#target-within-pseudo - | "target-within" + export interface TargetWithin extends PseudoClassSelector<"target-within"> {} + // https://www.w3.org/TR/selectors/#scope-pseudo - | "scope" + export interface Scope extends PseudoClassSelector<"scope"> {} + // https://www.w3.org/TR/selectors/#hover-pseudo - | "hover" + export interface Hover extends PseudoClassSelector<"hover"> {} + // https://www.w3.org/TR/selectors/#active-pseudo - | "active" + export interface Active extends PseudoClassSelector<"active"> {} + // https://www.w3.org/TR/selectors/#focus-pseudo - | "focus" + export interface Focus extends PseudoClassSelector<"focus"> {} + // https://www.w3.org/TR/selectors/#focus-visible-pseudo - | "focus-visible" + export interface FocusVisible extends PseudoClassSelector<"focus-visible"> {} + // https://www.w3.org/TR/selectors/#focus-within-pseudo - | "focus-within" - // https://www.w3.org/TR/selectors/#drag-pseudos - | "drop" + export interface FocusWithin extends PseudoClassSelector<"focus-within"> {} + // https://www.w3.org/TR/selectors/#current-pseudo - | "current" + export interface Current + extends PseudoClassSelector<"current">, + Partial {} + // https://www.w3.org/TR/selectors/#past-pseudo - | "past" + export interface Past extends PseudoClassSelector<"past"> {} + // https://www.w3.org/TR/selectors/#future-pseudo - | "future" + export interface Future extends PseudoClassSelector<"future"> {} + // https://www.w3.org/TR/selectors/#video-state - | "playing" - | "paused" + export interface Playing extends PseudoClassSelector<"playing"> {} + + export interface Paused extends PseudoClassSelector<"paused"> {} + // https://www.w3.org/TR/selectors/#enabled-pseudo - | "enabled" + export interface Enabled extends PseudoClassSelector<"enabled"> {} + // https://www.w3.org/TR/selectors/#disabled-pseudo - | "disabled" + export interface Disabled extends PseudoClassSelector<"disabled"> {} + // https://www.w3.org/TR/selectors/#read-only-pseudo - | "read-only" + export interface ReadOnly extends PseudoClassSelector<"read-only"> {} + // https://www.w3.org/TR/selectors/#read-write-pseudo - | "read-write" + export interface ReadWrite extends PseudoClassSelector<"read-write"> {} + // https://www.w3.org/TR/selectors/#placeholder-shown-pseudo - | "placeholder-shown" + export interface PlaceholderShown + extends PseudoClassSelector<"placeholder-shown"> {} + // https://www.w3.org/TR/selectors/#default-pseudo - | "default" + export interface Default extends PseudoClassSelector<"default"> {} + // https://www.w3.org/TR/selectors/#checked-pseudo - | "checked" + export interface Checked extends PseudoClassSelector<"checked"> {} + // https://www.w3.org/TR/selectors/#indetermine-pseudo - | "indetermine" + export interface Indetermine extends PseudoClassSelector<"indetermine"> {} + // https://www.w3.org/TR/selectors/#valid-pseudo - | "valid" + export interface Valid extends PseudoClassSelector<"valid"> {} + // https://www.w3.org/TR/selectors/#invalid-pseudo - | "invalid" + export interface Invalid extends PseudoClassSelector<"invalid"> {} + // https://www.w3.org/TR/selectors/#in-range-pseudo - | "in-range" + export interface InRange extends PseudoClassSelector<"in-range"> {} + // https://www.w3.org/TR/selectors/#out-of-range-pseudo - | "out-of-range" + export interface OutOfRange extends PseudoClassSelector<"out-of-range"> {} + // https://www.w3.org/TR/selectors/#required-pseudo - | "required" + export interface Required extends PseudoClassSelector<"required"> {} + // https://www.w3.org/TR/selectors/#user-invalid-pseudo - | "user-invalid" + export interface UserInvalid extends PseudoClassSelector<"user-invalid"> {} + // https://drafts.csswg.org/css-scoping/#host-selector - | "host" + export interface Host + extends PseudoClassSelector<"host">, + Partial {} + // https://drafts.csswg.org/css-scoping/#host-selector - | "host-context" + export interface HostContext + extends PseudoClassSelector<"host-context">, + WithSelector {} + // https://www.w3.org/TR/selectors/#root-pseudo - | "root" + export interface Root extends PseudoClassSelector<"root"> {} + // https://www.w3.org/TR/selectors/#empty-pseudo - | "empty" + export interface Empty extends PseudoClassSelector<"empty"> {} + // https://www.w3.org/TR/selectors/#blank-pseudo - | "blank" + export interface Blank extends PseudoClassSelector<"blank"> {} + // https://www.w3.org/TR/selectors/#first-child-pseudo - | "first-child" + export interface FirstChild extends PseudoClassSelector<"first-child"> {} + // https://www.w3.org/TR/selectors/#last-child-pseudo - | "last-child" + export interface LastChild extends PseudoClassSelector<"last-child"> {} + // https://www.w3.org/TR/selectors/#only-child-pseudo - | "only-child" + export interface OnlyChild extends PseudoClassSelector<"only-child"> {} + // https://www.w3.org/TR/selectors/#first-of-type-pseudo - | "first-of-type" + export interface FirstOfType extends PseudoClassSelector<"first-of-type"> {} + // https://www.w3.org/TR/selectors/#last-of-type-pseudo - | "last-of-type" + export interface LastOfType extends PseudoClassSelector<"last-of-type"> {} + // https://www.w3.org/TR/selectors/#only-of-type-pseudo - | "only-of-type" - // https://www.w3.org/TR/selectors/#child-index - | ChildIndexedPseudoClass; + export interface OnlyOfType extends PseudoClassSelector<"only-of-type"> {} -type ChildIndexedPseudoClass = // https://www.w3.org/TR/selectors/#nth-child-pseudo - | "nth-child" + export interface NthChild + extends PseudoClassSelector<"nth-child">, + WithChildIndex {} + // https://www.w3.org/TR/selectors/#nth-last-child-pseudo - | "nth-last-child" + export interface NthLastChild + extends PseudoClassSelector<"nth-last-child">, + WithChildIndex {} + // https://www.w3.org/TR/selectors/#nth-of-type-pseudo - | "nth-of-type" + export interface NthOfType + extends PseudoClassSelector<"nth-of-type">, + WithChildIndex {} + // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo - | "nth-last-of-type" + export interface NthLastOfType + extends PseudoClassSelector<"nth-last-of-type">, + WithChildIndex {} + // https://www.w3.org/TR/selectors/#nth-col-pseudo - | "nth-col" + export interface NthCol + extends PseudoClassSelector<"nth-col">, + WithChildIndex {} + // https://www.w3.org/TR/selectors/#nth-last-col-pseudo - | "nth-last-col"; + export interface NthLastCol + extends PseudoClassSelector<"nth-last-col">, + WithChildIndex {} +} + +export type PseudoClassSelector = + | PseudoClassSelector.Is + | PseudoClassSelector.Not + | PseudoClassSelector.Has + | PseudoClassSelector.Dir + | PseudoClassSelector.Lang + | PseudoClassSelector.AnyLink + | PseudoClassSelector.Link + | PseudoClassSelector.Visited + | PseudoClassSelector.LocalLink + | PseudoClassSelector.Target + | PseudoClassSelector.TargetWithin + | PseudoClassSelector.Scope + | PseudoClassSelector.Hover + | PseudoClassSelector.Active + | PseudoClassSelector.Focus + | PseudoClassSelector.FocusVisible + | PseudoClassSelector.FocusWithin + | PseudoClassSelector.Current + | PseudoClassSelector.Past + | PseudoClassSelector.Future + | PseudoClassSelector.Playing + | PseudoClassSelector.Paused + | PseudoClassSelector.Enabled + | PseudoClassSelector.Disabled + | PseudoClassSelector.ReadOnly + | PseudoClassSelector.ReadWrite + | PseudoClassSelector.PlaceholderShown + | PseudoClassSelector.Default + | PseudoClassSelector.Checked + | PseudoClassSelector.Valid + | PseudoClassSelector.Invalid + | PseudoClassSelector.InRange + | PseudoClassSelector.OutOfRange + | PseudoClassSelector.Required + | PseudoClassSelector.UserInvalid + | PseudoClassSelector.Host + | PseudoClassSelector.HostContext + | PseudoClassSelector.Root + | PseudoClassSelector.Empty + | PseudoClassSelector.Blank + | PseudoClassSelector.FirstChild + | PseudoClassSelector.LastChild + | PseudoClassSelector.OnlyChild + | PseudoClassSelector.FirstOfType + | PseudoClassSelector.LastOfType + | PseudoClassSelector.Indetermine + | PseudoClassSelector.OnlyOfType + | PseudoClassSelector.NthChild + | PseudoClassSelector.NthLastChild + | PseudoClassSelector.NthOfType + | PseudoClassSelector.NthLastChild + | PseudoClassSelector.NthLastOfType + | PseudoClassSelector.NthCol + | PseudoClassSelector.NthLastCol; + +export type PseudoClass = PseudoClassSelector["name"]; /** * @see https://www.w3.org/TR/selectors/#pseudo-elements */ -export type PseudoElement = +export namespace PseudoElementSelector { + interface PseudoElementSelector { + readonly type: SelectorType.PseudoElementSelector; + readonly name: T; + } + // https://www.w3.org/TR/css-pseudo/#first-line-pseudo - | "first-line" + export interface FirstLine extends PseudoElementSelector<"first-line"> {} + // https://www.w3.org/TR/css-pseudo/#first-letter-pseudo - | "first-letter" + export interface FirstLetter extends PseudoElementSelector<"first-letter"> {} + // https://www.w3.org/TR/css-pseudo/#highlight-pseudos - | "selection" - | "inactive-selection" - | "spelling-error" - | "grammar-error" + export interface Selection extends PseudoElementSelector<"selection"> {} + + export interface InactiveSelection + extends PseudoElementSelector<"inactive-selection"> {} + + export interface SpellingError + extends PseudoElementSelector<"spelling-error"> {} + + export interface GrammarError + extends PseudoElementSelector<"grammar-error"> {} + // https://www.w3.org/TR/css-pseudo/#generated-content - | "before" - | "after" + export interface Before extends PseudoElementSelector<"before"> {} + + export interface After extends PseudoElementSelector<"after"> {} + // https://www.w3.org/TR/css-pseudo/#marker-pseudo - | "marker" + export interface Marker extends PseudoElementSelector<"marker"> {} + // https://www.w3.org/TR/css-pseudo/#placeholder-pseudo - | "placeholder"; + export interface Placeholder extends PseudoElementSelector<"placeholder"> {} +} + +export type PseudoElementSelector = + | PseudoElementSelector.FirstLine + | PseudoElementSelector.FirstLetter + | PseudoElementSelector.Selection + | PseudoElementSelector.InactiveSelection + | PseudoElementSelector.SpellingError + | PseudoElementSelector.GrammarError + | PseudoElementSelector.Before + | PseudoElementSelector.After + | PseudoElementSelector.Marker + | PseudoElementSelector.Placeholder; + +export type PseudoElement = PseudoElementSelector["name"]; export interface ChildIndex { - readonly a: number; - readonly b: number; + readonly step: number; + readonly offset: number; } export const enum MediaQualifier { diff --git a/packages/alfa-css/test/alphabet.spec.ts b/packages/alfa-css/test/alphabet.spec.ts index b70a5e9456..91f00df30f 100644 --- a/packages/alfa-css/test/alphabet.spec.ts +++ b/packages/alfa-css/test/alphabet.spec.ts @@ -133,7 +133,19 @@ test("Can lex an integer", t => { { type: TokenType.Number, value: 123, - integer: true + integer: true, + signed: false + } + ]); +}); + +test("Can lex a positive integer", t => { + css(t, "+123", [ + { + type: TokenType.Number, + value: 123, + integer: true, + signed: true } ]); }); @@ -143,7 +155,8 @@ test("Can lex a negative integer", t => { { type: TokenType.Number, value: -123, - integer: true + integer: true, + signed: true } ]); }); @@ -153,7 +166,8 @@ test("Can lex a decimal", t => { { type: TokenType.Number, value: 123.456, - integer: false + integer: false, + signed: false } ]); }); @@ -163,7 +177,8 @@ test("Correctly lexes odd decimals", t => { { type: TokenType.Number, value: 0.3, - integer: false + integer: false, + signed: false } ]); }); @@ -173,7 +188,8 @@ test("Can lex a negative decimal", t => { { type: TokenType.Number, value: -123.456, - integer: false + integer: false, + signed: true } ]); }); @@ -183,7 +199,8 @@ test("Can lex a decimal in E-notation", t => { { type: TokenType.Number, value: 123.456e2, - integer: false + integer: false, + signed: false } ]); }); @@ -193,7 +210,8 @@ test("Can lex a negative decimal in E-notation", t => { { type: TokenType.Number, value: -123.456e2, - integer: false + integer: false, + signed: true } ]); }); @@ -203,7 +221,8 @@ test("Correctly lexes odd E-notations", t => { { type: TokenType.Number, value: 3e-1, - integer: false + integer: false, + signed: false } ]); }); @@ -214,6 +233,7 @@ test("Can lex a dimension", t => { type: TokenType.Dimension, value: 123, integer: true, + signed: false, unit: "px" } ]); @@ -250,7 +270,8 @@ test("Can lex a function with a single argument", t => { { type: TokenType.Number, value: 123, - integer: true + integer: true, + signed: false }, { type: TokenType.RightParenthesis @@ -338,40 +359,47 @@ test("Can lex an+b values", t => { type: TokenType.Dimension, value: 2, integer: true, + signed: false, unit: "n" }, { type: TokenType.Number, value: 4, - integer: true + integer: true, + signed: true } ]); + css(t, "n-4", [ { type: TokenType.Ident, value: "n-4" } ]); + css(t, "1n-4", [ { type: TokenType.Dimension, value: 1, integer: true, + signed: false, unit: "n-4" } ]); - "n-5"; + css(t, "-1n+5", [ { type: TokenType.Dimension, value: -1, integer: true, + signed: true, unit: "n" }, { type: TokenType.Number, value: 5, - integer: true + integer: true, + signed: true } ]); }); diff --git a/packages/alfa-css/test/grammars/child-index.spec.ts b/packages/alfa-css/test/grammars/child-index.spec.ts index c7628a09c9..4575580736 100644 --- a/packages/alfa-css/test/grammars/child-index.spec.ts +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -8,100 +8,294 @@ function childIndex(t: Assertions, input: string, expected: ChildIndex | null) { const lexer = lex(input, Alphabet); const parser = parse(lexer.result, ChildIndexGrammar); + if (expected !== null) { + t(parser.done, input); + } + t.deepEqual(parser.result, expected, input); } -test("Can parse a n-dimension signless-integer child index", t => { - childIndex(t, "2n+3", { - a: 2, - b: 3 +test("Can parse a child index", t => { + childIndex(t, "even", { + step: 2, + offset: 0 }); - childIndex(t, "n +3", { - a: 1, - b: 3 + childIndex(t, "odd", { + step: 2, + offset: 1 + }); + + childIndex(t, "7", { + step: 0, + offset: 7 + }); + + childIndex(t, "-7", { + step: 0, + offset: -7 }); -}); -test("Can parse a n-dimension child index", t => { childIndex(t, "n", { - a: 1, - b: 0 + step: 1, + offset: 0 }); - childIndex(t, "N", { - a: 1, - b: 0 + childIndex(t, "+n", { + step: 1, + offset: 0 }); childIndex(t, "-n", { - a: -1, - b: 0 + step: -1, + offset: 0 }); - childIndex(t, "--n", null); -}); + childIndex(t, "n+3", { + step: 1, + offset: 3 + }); -test("Can parse a even odd child index", t => { - childIndex(t, "even", { - a: 2, - b: 0 + childIndex(t, "n+ 3", { + step: 1, + offset: 3 }); - childIndex(t, "odd", { - a: 2, - b: 1 + childIndex(t, "n +3", { + step: 1, + offset: 3 }); -}); -test("Cannot parse a float", t => { - childIndex(t, "3.14", null); -}); + childIndex(t, "n + 3", { + step: 1, + offset: 3 + }); -test("Cannot parse a dimension with wrong unit", t => { - childIndex(t, "3px7", null); -}); + childIndex(t, "+n+3", { + step: 1, + offset: 3 + }); -test("Cannot parse a unknown ident", t => { - childIndex(t, "p", null); -}); + childIndex(t, "+n+ 3", { + step: 1, + offset: 3 + }); -test("Can parse a ndashdigit-dimension signed-integer child index", t => { - childIndex(t, "-2n-3", { - a: -2, - b: -3 + childIndex(t, "+n +3", { + step: 1, + offset: 3 }); -}); -test("Can parse a dashndashdigit-ident signed-integer child index", t => { - childIndex(t, "-n-3", { - a: -1, - b: -3 + childIndex(t, "+n + 3", { + step: 1, + offset: 3 + }); + + childIndex(t, "-n+3", { + step: -1, + offset: 3 + }); + + childIndex(t, "-n+ 3", { + step: -1, + offset: 3 + }); + + childIndex(t, "-n +3", { + step: -1, + offset: 3 + }); + + childIndex(t, "-n + 3", { + step: -1, + offset: 3 }); -}); -test("Can parse a ndashdigit-ident signed-integer child index", t => { childIndex(t, "n-3", { - a: 1, - b: -3 + step: 1, + offset: -3 }); -}); -test("Can parse a ndashdigit-dimension singless-integer child index", t => { - childIndex(t, "-2n3", { - a: -2, - b: 3 + childIndex(t, "n- 3", { + step: 1, + offset: -3 }); -}); -test("Can parse a number", t => { - childIndex(t, "7", { - a: 0, - b: 7 + childIndex(t, "n -3", { + step: 1, + offset: -3 }); - childIndex(t, "-7", { - a: 0, - b: -7 + childIndex(t, "n - 3", { + step: 1, + offset: -3 + }); + + childIndex(t, "+n-3", { + step: 1, + offset: -3 + }); + + childIndex(t, "+n- 3", { + step: 1, + offset: -3 + }); + + childIndex(t, "+n -3", { + step: 1, + offset: -3 + }); + + childIndex(t, "+n - 3", { + step: 1, + offset: -3 + }); + + childIndex(t, "-n-3", { + step: -1, + offset: -3 + }); + + childIndex(t, "-n- 3", { + step: -1, + offset: -3 + }); + + childIndex(t, "-n -3", { + step: -1, + offset: -3 }); + + childIndex(t, "-n - 3", { + step: -1, + offset: -3 + }); + + childIndex(t, "2n+3", { + step: 2, + offset: 3 + }); + + childIndex(t, "2n+ 3", { + step: 2, + offset: 3 + }); + + childIndex(t, "2n +3", { + step: 2, + offset: 3 + }); + + childIndex(t, "2n + 3", { + step: 2, + offset: 3 + }); + + childIndex(t, "+2n+3", { + step: 2, + offset: 3 + }); + + childIndex(t, "+2n+ 3", { + step: 2, + offset: 3 + }); + + childIndex(t, "+2n +3", { + step: 2, + offset: 3 + }); + + childIndex(t, "+2n + 3", { + step: 2, + offset: 3 + }); + + childIndex(t, "-2n+3", { + step: -2, + offset: 3 + }); + + childIndex(t, "-2n+ 3", { + step: -2, + offset: 3 + }); + + childIndex(t, "-2n +3", { + step: -2, + offset: 3 + }); + + childIndex(t, "-2n + 3", { + step: -2, + offset: 3 + }); + + childIndex(t, "2n-3", { + step: 2, + offset: -3 + }); + + childIndex(t, "2n- 3", { + step: 2, + offset: -3 + }); + + childIndex(t, "2n -3", { + step: 2, + offset: -3 + }); + + childIndex(t, "2n - 3", { + step: 2, + offset: -3 + }); + + childIndex(t, "+2n-3", { + step: 2, + offset: -3 + }); + + childIndex(t, "+2n- 3", { + step: 2, + offset: -3 + }); + + childIndex(t, "+2n -3", { + step: 2, + offset: -3 + }); + + childIndex(t, "+2n - 3", { + step: 2, + offset: -3 + }); + + childIndex(t, "-2n-3", { + step: -2, + offset: -3 + }); + + childIndex(t, "-2n- 3", { + step: -2, + offset: -3 + }); + + childIndex(t, "-2n -3", { + step: -2, + offset: -3 + }); + + childIndex(t, "-2n - 3", { + step: -2, + offset: -3 + }); +}); + +test("Cannot parse an invalid child index", t => { + const invalid = ["3.14", "3px", "3px7", "px", "-2n3+3"]; + + for (const input of invalid) { + childIndex(t, input, null); + } }); diff --git a/packages/alfa-css/test/grammars/declaration.spec.ts b/packages/alfa-css/test/grammars/declaration.spec.ts index 261f672c6e..c670e59f16 100644 --- a/packages/alfa-css/test/grammars/declaration.spec.ts +++ b/packages/alfa-css/test/grammars/declaration.spec.ts @@ -47,6 +47,7 @@ test("Can parse a list of declarations", t => { type: TokenType.Dimension, value: 24, integer: true, + signed: false, unit: "px" } ], diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index b2311ec9af..13053d7118 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -680,10 +680,12 @@ test("Can parse a functional pseudo-class selector", t => { selector(t, ":not(.foo)", { type: SelectorType.PseudoClassSelector, name: "not", - value: { - type: SelectorType.ClassSelector, - name: "foo" - } + value: [ + { + type: SelectorType.ClassSelector, + name: "foo" + } + ] }); }); @@ -817,8 +819,8 @@ test("Can parse selector with an An+B odd microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 2, - b: 1 + step: 2, + offset: 1 } }; selector(t, ":nth-child(2n+1)", expected); @@ -831,8 +833,8 @@ test("Can parse selector with an An+B even microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 2, - b: 0 + step: 2, + offset: 0 } }; selector(t, ":nth-child(2n+0)", expected); @@ -844,8 +846,8 @@ test("Can parse selector using only 'An' from the An+B microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 2, - b: 0 + step: 2, + offset: 0 } }; selector(t, ":nth-child(2n)", expected); @@ -856,16 +858,16 @@ test("Can parse selector using only 'n' from the An+B microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 1, - b: 0 + step: 1, + offset: 0 } }); selector(t, ":nth-child(-n-0)", { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: -1, - b: -0 + step: -1, + offset: -0 } }); }); @@ -875,8 +877,8 @@ test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 1, - b: 2 + step: 1, + offset: 2 } }; selector(t, ":nth-child(n+2)", expected); @@ -887,8 +889,8 @@ test("Can parse selector using only 'B' from the An+B microsyntax", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 0, - b: 2 + step: 0, + offset: 2 } }; selector(t, ":nth-child(2)", expected); @@ -899,8 +901,8 @@ test("Can parse selector with an An+B microsyntax with negative integers", t => type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: -2, - b: -3 + step: -2, + offset: -3 } }; selector(t, ":nth-child(-2n-3)", expected); @@ -911,8 +913,8 @@ test("Can parse selector with an An+B microsyntax with whitespace", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 2, - b: 3 + step: 2, + offset: 3 } }); selector(t, ":nth-child(- n+3)", null); @@ -920,8 +922,8 @@ test("Can parse selector with an An+B microsyntax with whitespace", t => { type: SelectorType.PseudoClassSelector, name: "nth-child", value: { - a: 2, - b: 0 + step: 2, + offset: 0 } }); }); diff --git a/packages/alfa-dom/src/matches.ts b/packages/alfa-dom/src/matches.ts index 7edc96933e..9a0a207fbf 100644 --- a/packages/alfa-dom/src/matches.ts +++ b/packages/alfa-dom/src/matches.ts @@ -2,7 +2,6 @@ import { AttributeMatcher, AttributeModifier, AttributeSelector, - ChildIndex, ClassSelector, CompoundSelector, IdSelector, @@ -629,10 +628,6 @@ function matchesPseudoClass( options: MatchesOptions, root: Selector ): boolean { - if (selector.value !== null && isChildIndexSyntax(selector.value)) { - return false; - } - switch (selector.name) { // https://www.w3.org/TR/selectors/#scope-pseudo case "scope": @@ -659,7 +654,7 @@ function matchesPseudoClass( // Match host with possible selector argument (e.g. ":host(.foo)") return ( - selector.value === null || + selector.value === undefined || matches(element, context, selector.value, options, root) ); } @@ -788,15 +783,3 @@ function canReject(selector: Selector, filter: AncestorFilter): boolean { return false; } - -/** - * Check if a selector is of interface AnBMicrosyntax. - */ -function isChildIndexSyntax( - selector: ChildIndex | Selector | Array -): selector is ChildIndex { - return ( - (selector).a !== undefined && - (selector).b !== undefined - ); -} diff --git a/packages/alfa-lang/src/parse.ts b/packages/alfa-lang/src/parse.ts index 0c590eb49b..3b871902ef 100644 --- a/packages/alfa-lang/src/parse.ts +++ b/packages/alfa-lang/src/parse.ts @@ -20,9 +20,13 @@ export function parse( if (input instanceof Stream) { stream = input; } else { - const readToken: (i: number) => T = i => input[i]; - - stream = new Stream(input.length, readToken, offset); + stream = new Stream( + input.length, + function readToken(i: number): T { + return input[i]; + }, + offset + ); } const state = grammar.state(); From cbaaee2692b43ca76f1233201b1ea9ebf1ea1a4d Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Wed, 7 Aug 2019 11:59:36 +0200 Subject: [PATCH 21/22] Fix undefined reference --- packages/alfa-css/src/grammars/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alfa-css/src/grammars/selector.ts b/packages/alfa-css/src/grammars/selector.ts index da643ba90e..ffd3c568ff 100644 --- a/packages/alfa-css/src/grammars/selector.ts +++ b/packages/alfa-css/src/grammars/selector.ts @@ -265,7 +265,7 @@ function pseudoSelector( case "placeholder": return { type: SelectorType.PseudoElementSelector, - name + name: next.value }; default: From 42c86cccd2eac5706052e62fb047cc95b3648608 Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Wed, 7 Aug 2019 12:34:00 +0200 Subject: [PATCH 22/22] Fix some test cases --- packages/alfa-css/test/grammars/selector.spec.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/alfa-css/test/grammars/selector.spec.ts b/packages/alfa-css/test/grammars/selector.spec.ts index 13053d7118..1d62296647 100644 --- a/packages/alfa-css/test/grammars/selector.spec.ts +++ b/packages/alfa-css/test/grammars/selector.spec.ts @@ -671,8 +671,7 @@ test("Only allows pseudo-element selectors as the last selector", t => { test("Can parse a named pseudo-class selector", t => { selector(t, ":hover", { type: SelectorType.PseudoClassSelector, - name: "hover", - value: null + name: "hover" }); }); @@ -699,8 +698,7 @@ test("Can parse a pseudo-class selector when part of a compound selector", t => }, right: { type: SelectorType.PseudoClassSelector, - name: "hover", - value: null + name: "hover" } }); }); @@ -722,8 +720,7 @@ test("Can parse a pseudo-class selector when part of a compound selector relativ }, right: { type: SelectorType.PseudoClassSelector, - name: "hover", - value: null + name: "hover" } } }); @@ -752,8 +749,7 @@ test("Can parse a compound type, class, and pseudo-class selector relative to a }, right: { type: SelectorType.PseudoClassSelector, - name: "hover", - value: null + name: "hover" } } }