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 new file mode 100644 index 0000000000..131ff3e824 --- /dev/null +++ b/packages/alfa-css/src/grammars/child-index.ts @@ -0,0 +1,229 @@ +import * as Lang from "@siteimprove/alfa-lang"; +import { Char, Grammar, isNumeric, skip, Stream } from "@siteimprove/alfa-lang"; +import { Token, Tokens, TokenType } from "../alphabet"; +import { ChildIndex } from "../types"; + +type Production = Lang.Production; + +const number: Production = { + token: TokenType.Number, + prefix(token, stream) { + if (!token.integer) { + return null; + } + + return { + step: 0, + offset: token.value + }; + } +}; + +const dimension: Production = { + token: TokenType.Dimension, + prefix(token, stream) { + if (!token.integer) { + return null; + } + + const unit = token.unit.toLowerCase(); + + switch (unit) { + case "n": + return { step: token.value, offset: parseOffset(stream) }; + + case "n-": + return { step: token.value, offset: -1 * parseOffset(stream, true) }; + } + + { + const stream = new Stream(unit.length, i => unit.charCodeAt(i)); + + let next = stream.peek(0); + + if (next === null || next !== Char.SmallLetterN) { + return null; + } + + stream.advance(1); + next = stream.peek(0); + + if (next === null || next !== Char.HyphenMinus) { + 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: token.value, offset: -1 * offset }; + } + + return null; + } +}; + +const ident: Production = { + token: TokenType.Ident, + prefix(token, stream) { + const value = token.value.toLowerCase(); + + switch (value) { + case "n": + return { step: 1, offset: parseOffset(stream) }; + + 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 }; + } + + { + 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; + } + + stream.advance(1); + next = stream.peek(0); + + if (next === null || next !== Char.HyphenMinus) { + 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 }; + } + } +}; + +const delim: Production = { + token: TokenType.Delim, + prefix(token, stream, expression) { + switch (token.value) { + case Char.PlusSign: { + const next = stream.peek(0); + + if (next === null || next.type !== TokenType.Ident) { + break; + } + + return expression(); + } + } + + return null; + } +}; + +export const ChildIndexGrammar: Grammar = new Lang.Grammar( + [[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 e918b71754..ffd3c568ff 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, + parse, + Stream +} from "@siteimprove/alfa-lang"; import { Token, Tokens, TokenType } from "../alphabet"; import { AttributeMatcher, @@ -9,9 +15,7 @@ import { ComplexSelector, CompoundSelector, IdSelector, - PseudoClass, PseudoClassSelector, - PseudoElement, PseudoElementSelector, RelativeSelector, Selector, @@ -20,6 +24,7 @@ import { SimpleSelector, TypeSelector } from "../types"; +import { ChildIndexGrammar } from "./child-index"; const { isArray } = Array; @@ -234,8 +239,6 @@ function pseudoSelector( stream: Stream, expression: Expression> ): PseudoElementSelector | PseudoClassSelector | null { - let selector: PseudoElementSelector | PseudoClassSelector; - let next = stream.next(); if (next === null) { @@ -249,8 +252,6 @@ function pseudoSelector( return null; } - let name: PseudoElement; - switch (next.value) { case "first-line": case "first-letter": @@ -262,27 +263,85 @@ function pseudoSelector( case "after": case "marker": case "placeholder": - name = next.value; - break; + return { + type: SelectorType.PseudoElementSelector, + name: next.value + }; + 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; + } - switch (next.value) { - case "matches": + return null; +} + +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": @@ -295,7 +354,6 @@ function pseudoSelector( case "focus": case "focus-visible": case "focus-within": - case "drop": case "current": case "past": case "future": @@ -309,6 +367,11 @@ function pseudoSelector( case "default": case "checked": case "indetermine": + return { + type: SelectorType.PseudoClassSelector, + name + }; + case "valid": case "invalid": case "in-range": @@ -316,44 +379,23 @@ function pseudoSelector( case "required": case "user-invalid": case "host": - case "host-context": 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-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 { - const 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 db4bc294b0..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 | null; -} - -export interface PseudoElementSelector { - readonly type: SelectorType.PseudoElementSelector; - readonly name: PseudoElement; -} - export type SimpleSelector = | IdSelector | ClassSelector @@ -165,137 +154,317 @@ 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" - // https://www.w3.org/TR/selectors/#nth-child-pseudo - | "nth-child" - // https://www.w3.org/TR/selectors/#nth-last-child-pseudo - | "nth-last-child" + 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" - // 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" + 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" + export interface OnlyOfType extends PseudoClassSelector<"only-of-type"> {} + + // https://www.w3.org/TR/selectors/#nth-child-pseudo + export interface NthChild + extends PseudoClassSelector<"nth-child">, + WithChildIndex {} + + // https://www.w3.org/TR/selectors/#nth-last-child-pseudo + export interface NthLastChild + extends PseudoClassSelector<"nth-last-child">, + WithChildIndex {} + + // https://www.w3.org/TR/selectors/#nth-of-type-pseudo + export interface NthOfType + extends PseudoClassSelector<"nth-of-type">, + WithChildIndex {} + + // https://www.w3.org/TR/selectors/#nth-last-of-type-pseudo + 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 step: number; + readonly offset: number; +} export const enum MediaQualifier { Only, diff --git a/packages/alfa-css/test/alphabet.spec.ts b/packages/alfa-css/test/alphabet.spec.ts index 79697d5bf3..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,12 +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" + } + ]); + + css(t, "-1n+5", [ + { + type: TokenType.Dimension, + value: -1, + integer: true, + signed: true, + unit: "n" + }, + { + type: TokenType.Number, + value: 5, + 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 new file mode 100644 index 0000000000..4575580736 --- /dev/null +++ b/packages/alfa-css/test/grammars/child-index.spec.ts @@ -0,0 +1,301 @@ +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); + + if (expected !== null) { + t(parser.done, input); + } + + t.deepEqual(parser.result, expected, input); +} + +test("Can parse a child index", t => { + childIndex(t, "even", { + step: 2, + offset: 0 + }); + + childIndex(t, "odd", { + step: 2, + offset: 1 + }); + + childIndex(t, "7", { + step: 0, + offset: 7 + }); + + childIndex(t, "-7", { + step: 0, + offset: -7 + }); + + childIndex(t, "n", { + step: 1, + offset: 0 + }); + + childIndex(t, "+n", { + step: 1, + offset: 0 + }); + + childIndex(t, "-n", { + step: -1, + offset: 0 + }); + + 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, "-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, "+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 106d79671a..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" }); }); @@ -680,10 +679,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" + } + ] }); }); @@ -697,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" } }); }); @@ -720,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" } } }); @@ -750,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" } } } @@ -811,3 +809,124 @@ test("Can parse a relative selector relative to a compound selector", t => { } }); }); + +test("Can parse selector with an An+B odd microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: 2, + offset: 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: { + step: 2, + offset: 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: { + step: 2, + offset: 0 + } + }; + selector(t, ":nth-child(2n)", expected); +}); + +test("Can parse selector using only 'n' from the An+B microsyntax", t => { + selector(t, ":nth-child(n)", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: 1, + offset: 0 + } + }); + selector(t, ":nth-child(-n-0)", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: -1, + offset: -0 + } + }); +}); + +test("Can parse selector omitting 'A' integer from the An+B microsyntax", t => { + const expected: Selector = { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: 1, + offset: 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: { + step: 0, + offset: 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: { + step: -2, + offset: -3 + } + }; + selector(t, ":nth-child(-2n-3)", expected); +}); + +test("Can parse selector with an An+B microsyntax with whitespace", t => { + selector(t, ":nth-child( 2n +3 )", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: 2, + offset: 3 + } + }); + selector(t, ":nth-child(- n+3)", null); + selector(t, ":nth-child( even )", { + type: SelectorType.PseudoClassSelector, + name: "nth-child", + value: { + step: 2, + offset: 0 + } + }); +}); + +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); +}); 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", diff --git a/packages/alfa-dom/src/matches.ts b/packages/alfa-dom/src/matches.ts index d87967554d..9a0a207fbf 100644 --- a/packages/alfa-dom/src/matches.ts +++ b/packages/alfa-dom/src/matches.ts @@ -165,6 +165,8 @@ export function matches( case SelectorType.PseudoElementSelector: return matchesPseudoElement(element, context, selector, options, root); } + + return false; } /** @@ -516,6 +518,8 @@ function matchesRelative( root ); } + + return false; } /** @@ -650,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) ); } diff --git a/packages/alfa-lang/src/parse.ts b/packages/alfa-lang/src/parse.ts index 46a3e2798e..3b871902ef 100644 --- a/packages/alfa-lang/src/parse.ts +++ b/packages/alfa-lang/src/parse.ts @@ -11,13 +11,23 @@ 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 stream = new Stream(input.length, readToken, offset); + let stream: Stream; + + if (input instanceof Stream) { + stream = input; + } else { + stream = new Stream( + input.length, + function readToken(i: number): T { + return input[i]; + }, + offset + ); + } const state = grammar.state();