Skip to content

Commit

Permalink
Accept "of selector" syntax in :nth-[last]-child pseudo-classes (#1524
Browse files Browse the repository at this point in the history
)

* Add (empty) selector to :nth-child

* Accept of selector syntax (with no effect)

* Improve nth

* Make selector optional in JSON

* Fix specificity of :nth-child

* Add of selector syntax for :nth-last-child

* Fix matching
  • Loading branch information
Jym77 authored Dec 7, 2023
1 parent d3f1178 commit ff300f9
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 107 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-horses-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-selector": minor
---

**Added:** The `:nth-child` and `:nth-last-child` pseudo-classes now accept the "of selector" syntax.
1 change: 1 addition & 0 deletions docs/review/api/alfa-selector.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Hash } from '@siteimprove/alfa-hash';
import { Hashable } from '@siteimprove/alfa-hash';
import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable';
import * as json from '@siteimprove/alfa-json';
import { Maybe } from '@siteimprove/alfa-option';
import { Nth } from '@siteimprove/alfa-css';
import { Option } from '@siteimprove/alfa-option';
import { Parser } from '@siteimprove/alfa-parser';
Expand Down
2 changes: 1 addition & 1 deletion packages/alfa-selector/src/selector/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export abstract class Selector<T extends string = string>

export namespace Selector {
export interface JSON<T extends string = string> {
[key: string]: json.JSON;
[key: string]: json.JSON | undefined;

type: T;
specificity: Specificity.JSON;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export namespace PseudoClass {
OnlyOfType.parse,
Root.parse,
Visited.parse,
NthChild.parse,
NthLastChild.parse,
NthChild.parse(parseSelector),
NthLastChild.parse(parseSelector),
NthLastOfType.parse,
NthOfType.parse,
Has.parse(parseSelector),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,79 @@
import type { Parser as CSSParser } from "@siteimprove/alfa-css";
import type { Nth } from "@siteimprove/alfa-css";
import { Element } from "@siteimprove/alfa-dom";
import { Maybe, None, Option } from "@siteimprove/alfa-option";
import type { Thunk } from "@siteimprove/alfa-thunk";

import { WithIndex } from "./pseudo-class";
import type { Context } from "../../../context";
import { Universal } from "../../index";

import type { Absolute } from "../../index";

import { WithIndexAndSelector } from "./pseudo-class";

const { isElement } = Element;

/**
* {@link https://drafts.csswg.org/selectors/#nth-child-pseudo}
*/
export class NthChild extends WithIndex<"nth-child"> {
public static of(index: Nth): NthChild {
return new NthChild(index);
export class NthChild extends WithIndexAndSelector<"nth-child"> {
public static of(index: Nth, selector: Maybe<Absolute> = None): NthChild {
return new NthChild(index, Maybe.toOption(selector));
}

private constructor(index: Nth) {
super("nth-child", index);
private readonly _indices = new WeakMap<Element, number>();

private constructor(index: Nth, selector: Option<Absolute>) {
super("nth-child", index, selector);
}

/** @public (knip) */
public *[Symbol.iterator](): Iterator<NthChild> {
yield this;
}

public matches(element: Element): boolean {
const indices = NthChild._indices;

if (!indices.has(element)) {
public matches(element: Element, context?: Context): boolean {
if (!this._indices.has(element)) {
element
.inclusiveSiblings()
.filter(isElement)
.filter((element) =>
this._selector
.getOr(Universal.of(Option.of("*")))
.matches(element, context),
)
.forEach((element, i) => {
indices.set(element, i + 1);
this._indices.set(element, i + 1);
});
}

return this._index.matches(indices.get(element)!);
if (!this._indices.has(element)) {
return false;
}

return this._index.matches(this._indices.get(element)!);
}

public equals(value: NthChild): boolean;

public equals(value: unknown): value is this;

public equals(value: unknown): boolean {
return value instanceof NthChild && value._index.equals(this._index);
return value instanceof NthChild && super.equals(value);
}

public toJSON(): NthChild.JSON {
return {
...super.toJSON(),
};
return super.toJSON();
}
}

export namespace NthChild {
export interface JSON extends WithIndex.JSON<"nth-child"> {}
export interface JSON extends WithIndexAndSelector.JSON<"nth-child"> {}

export const parse = WithIndex.parseWithIndex("nth-child", NthChild.of);
export const parse = (parseSelector: Thunk<CSSParser<Absolute>>) =>
WithIndexAndSelector.parseWithIndexAndSelector(
"nth-child",
parseSelector,
NthChild.of,
);
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,80 @@
import type { Parser as CSSParser } from "@siteimprove/alfa-css";
import type { Nth } from "@siteimprove/alfa-css";
import { Element } from "@siteimprove/alfa-dom";
import { Maybe, None, Option } from "@siteimprove/alfa-option";
import type { Thunk } from "@siteimprove/alfa-thunk";

import { WithIndex } from "./pseudo-class";
import type { Context } from "../../../context";
import { Universal } from "../../index";

import type { Absolute } from "../../index";

import { WithIndexAndSelector } from "./pseudo-class";

const { isElement } = Element;

/**
* {@link https://drafts.csswg.org/selectors/#nth-last-child-pseudo}
*/
export class NthLastChild extends WithIndex<"nth-last-child"> {
public static of(index: Nth): NthLastChild {
return new NthLastChild(index);
export class NthLastChild extends WithIndexAndSelector<"nth-last-child"> {
public static of(index: Nth, selector: Maybe<Absolute> = None): NthLastChild {
return new NthLastChild(index, Maybe.toOption(selector));
}

private constructor(nth: Nth) {
super("nth-last-child", nth);
private readonly _indices = new WeakMap<Element, number>();

private constructor(nth: Nth, selector: Option<Absolute>) {
super("nth-last-child", nth, selector);
}

/** @public (knip) */
public *[Symbol.iterator](): Iterator<NthLastChild> {
yield this;
}

public matches(element: Element): boolean {
const indices = NthLastChild._indices;

if (!indices.has(element)) {
public matches(element: Element, context?: Context): boolean {
if (!this._indices.has(element)) {
element
.inclusiveSiblings()
.filter(isElement)
.filter((element) =>
this._selector
.getOr(Universal.of(Option.of("*")))
.matches(element, context),
)
.reverse()
.forEach((element, i) => {
indices.set(element, i + 1);
this._indices.set(element, i + 1);
});
}

return this._index.matches(indices.get(element)!);
if (!this._indices.has(element)) {
return false;
}

return this._index.matches(this._indices.get(element)!);
}

public equals(value: NthLastChild): boolean;

public equals(value: unknown): value is this;

public equals(value: unknown): boolean {
return value instanceof NthLastChild && value._index.equals(this._index);
return value instanceof NthLastChild && super.equals(value);
}

public toJSON(): NthLastChild.JSON {
return {
...super.toJSON(),
};
return super.toJSON();
}
}

export namespace NthLastChild {
export interface JSON extends WithIndex.JSON<"nth-last-child"> {}
export interface JSON extends WithIndexAndSelector.JSON<"nth-last-child"> {}

export const parse = WithIndex.parseWithIndex(
"nth-last-child",
NthLastChild.of,
);
export const parse = (parseSelector: Thunk<CSSParser<Absolute>>) =>
WithIndexAndSelector.parseWithIndexAndSelector(
"nth-last-child",
parseSelector,
NthLastChild.of,
);
}
Loading

0 comments on commit ff300f9

Please sign in to comment.