From 18dcc17037b10f21bafc82521ae980930ac4a35a Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 11:26:55 +0100 Subject: [PATCH 01/26] Move canReject as an AncestorFilter method --- packages/alfa-cascade/src/ancestor-filter.ts | 75 +++++++++++++++++--- packages/alfa-cascade/src/selector-map.ts | 74 +++++++------------ 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/packages/alfa-cascade/src/ancestor-filter.ts b/packages/alfa-cascade/src/ancestor-filter.ts index 74914a0b1c..3765dbf4cf 100644 --- a/packages/alfa-cascade/src/ancestor-filter.ts +++ b/packages/alfa-cascade/src/ancestor-filter.ts @@ -1,6 +1,15 @@ import { Element } from "@siteimprove/alfa-dom"; +import { Iterable } from "@siteimprove/alfa-iterable"; import { Serializable } from "@siteimprove/alfa-json"; -import { Class, Id, Selector, Type } from "@siteimprove/alfa-selector"; +import { + Class, + Combinator, + Complex, + Compound, + Id, + Selector, + Type, +} from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; @@ -43,13 +52,14 @@ import * as json from "@siteimprove/alfa-json"; * the filter acts as a quick guaranteed rejection mechanism, but actual match * test is needed to have an accurate final result. * - * NB: None of the operations of the ancestor filter are idempotent to avoid - * keeping track of more information than strictly necessary. This is however - * not a problem when ancestor filters are used during top-down traversal of the - * DOM, in which case an element is only ever visited once. If used elsewhere - * care must however be taken when adding and removing elements; elements must - * only ever be added and removed once, and an element must not be removed - * before being added. + * @privateRemarks + * Ancestor filters are mutable! None of the operations of the ancestor filter + * are idempotent to avoid keeping track of more information than strictly + * necessary. This is however not a problem when ancestor filters are used + * during top-down traversal of the DOM, in which case an element is only ever + * visited once. If used elsewhere care must however be taken when adding and + * removing elements; elements must only ever be added and removed once, and + * an element must not be removed before being added. * * {@link http://doc.servo.org/style/bloom/struct.StyleBloom.html} * @@ -106,6 +116,44 @@ export class AncestorFilter implements Serializable { return false; } + /** + * Check if a selector can be rejected based on the ancestor filter. + */ + public canReject(selector: Selector): boolean { + // If the selector is a simple selector, it must be in the filter in order to match. + if (Id.isId(selector) || Class.isClass(selector) || Type.isType(selector)) { + return !this.matches(selector); + } + + // If the selector is a compound selector, each of its component must be in + // the filter in order to match + if (Compound.isCompound(selector)) { + // Compound selectors are right-leaning, so recurse to the left first as it + // is likely the shortest branch. + return Iterable.some(selector.selectors, (selector) => + this.canReject(selector), + ); + } + + // If the selector is a complex selector with a descendant combinator, rejecting + // one side of the combinator is enough to reject the full selector. + // Sibling combinator are not handled by ancestor filters. + if (Complex.isComplex(selector)) { + const { combinator } = selector; + + if ( + combinator === Combinator.Descendant || + combinator === Combinator.DirectDescendant + ) { + // Complex selectors are left-leaning, so recurse to the right first as it + // is likely the shortest branch. + return this.canReject(selector.right) || this.canReject(selector.left); + } + } + + return false; + } + public toJSON(): AncestorFilter.JSON { return { ids: this._ids.toJSON(), @@ -129,9 +177,11 @@ export namespace AncestorFilter { /** * An ancestor bucket stores entries with associated counts in order to keep - * track of how many elements are associated with the entry. When the number of - * elements associated with a given entry drops to zero then the entry can be - * removed from the bucket. + * track of how many elements are associated with the entry. + * + * @remarks + * When the number of elements associated with a given entry drops to zero then + * the entry can be removed from the bucket. * * While most browser implementations use bloom filters for ancestor filters, we * can make do with native maps for two reasons: Memory is not much of a concern @@ -139,6 +189,9 @@ export namespace AncestorFilter { * actually much faster than any bloom filter we might be able to cook up in * plain JavaScript. * + * @privateRemarks + * Buckets are mutable! Adding or removing elements happens by side effect. + * * @internal */ export class Bucket implements Serializable { diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index f72df8416e..3d401f00ac 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -68,24 +68,34 @@ enum Origin { /** * The selector map is a data structure used for providing indexed access to the - * rules that are likely to match a given element. Rules are indexed according - * to their key selector, which is the selector that a given element MUST match - * in order for the rest of the selector to also match. A key selector can be - * either an ID selector, a class selector, or a type selector. In a relative - * selector, the key selector will be the right-most selector, e.g. given - * `main .foo + div` the key selector would be `div`. In a compound selector, - * the key selector will be left-most selector, e.g. given `div.foo` the key - * selector would also be `div`. + * rules that are likely to match a given element. * + * @remarks + * Rules are indexed according to their key selector, which is the selector + * that a given element MUST match in order for the rest of the selector to also + * match. A key selector can be either an ID selector, a class selector, or a + * type selector. In a relative selector, the key selector will be the + * right-most selector, e.g. given `main .foo + div` the key selector would be + * `div`. In a compound selector, the key selector will be left-most selector, + * e.g. given `div.foo` the key selector would also be `div`. + * + * Any element matching a selector must match its key selector. E.g., anything + * matching `main .foo + div` must be a `div`. Reciprocally, a `
` + * can only match selectors whose key selector is `div` or `bar`. Thus, filtering + * on key selectors decrease the search space for matching selector before the + * computation heavy steps of traversing the DOM to loo for siblings or ancestors. + * + * @privateRemarks * Internally, the selector map has three maps and a list in one of which it * will store a given selector. The three maps are used for selectors for which * a key selector exist; one for ID selectors, one for class selectors, and one - * for type selectors. The list is used for any remaining selectors. When - * looking up the rules that match an element, the ID, class names, and type of - * the element are used for looking up potentially matching selectors in the - * three maps. Selector matching is then performed against this list of - * potentially matching selectors, plus the list of remaining selectors, in - * order to determine the final set of matches. + * for type selectors. The list is used for any remaining selectors (e.g., + * pseudo-classes and -elements selectors have no key selector). When looking + * up the rules that match an element, the ID, class names, and type of the + * element are used for looking up potentially matching selectors in the three + * maps. Selector matching is then performed against this list of potentially + * matching selectors, plus the list of remaining selectors, in order to + * determine the final set of matches. * * {@link http://doc.servo.org/style/selector_map/struct.SelectorMap.html} * @@ -132,7 +142,7 @@ export class SelectorMap implements Serializable { Iterable.every( node.selector, and(isDescendantSelector, (selector) => - canReject(selector.left, filter), + filter.canReject(selector.left), ), ), ) && @@ -421,37 +431,3 @@ export namespace SelectorMap { export type JSON = Array<[string, Array]>; } } - -/** - * Check if a selector can be rejected based on an ancestor filter. - */ -function canReject(selector: Selector, filter: AncestorFilter): boolean { - if (isId(selector) || isClass(selector) || isType(selector)) { - return !filter.matches(selector); - } - - if (isCompound(selector)) { - // Compound selectors are right-leaning, so recurse to the left first as it - // is likely the shortest branch. - return Iterable.some(selector.selectors, (selector) => - canReject(selector, filter), - ); - } - - if (isComplex(selector)) { - const { combinator } = selector; - - if ( - combinator === Combinator.Descendant || - combinator === Combinator.DirectDescendant - ) { - // Complex selectors are left-leaning, so recurse to the right first as it - // is likely the shortest branch. - return ( - canReject(selector.right, filter) || canReject(selector.left, filter) - ); - } - } - - return false; -} From 31f390c154bc68edb7ea27e0a508f38fdc76f963 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 12:56:58 +0100 Subject: [PATCH 02/26] Start building Precedence interface --- .changeset/wet-stingrays-tap.md | 5 +++ packages/alfa-cascade/src/precedence/index.ts | 2 + .../alfa-cascade/src/precedence/origin.ts | 19 +++++++++ .../alfa-cascade/src/precedence/precedence.ts | 40 +++++++++++++++++++ packages/alfa-cascade/src/selector-map.ts | 38 ++++-------------- packages/alfa-cascade/tsconfig.json | 3 ++ 6 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 .changeset/wet-stingrays-tap.md create mode 100644 packages/alfa-cascade/src/precedence/index.ts create mode 100644 packages/alfa-cascade/src/precedence/origin.ts create mode 100644 packages/alfa-cascade/src/precedence/precedence.ts diff --git a/.changeset/wet-stingrays-tap.md b/.changeset/wet-stingrays-tap.md new file mode 100644 index 0000000000..f6115dac2d --- /dev/null +++ b/.changeset/wet-stingrays-tap.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Added:** Functionalities for dealing with Cascade Sorting Order (origin, specificicy, order) are now grouped in a `Precedence` interface. diff --git a/packages/alfa-cascade/src/precedence/index.ts b/packages/alfa-cascade/src/precedence/index.ts new file mode 100644 index 0000000000..65d43a10ff --- /dev/null +++ b/packages/alfa-cascade/src/precedence/index.ts @@ -0,0 +1,2 @@ +export * from "./origin"; +export * from "./precedence"; diff --git a/packages/alfa-cascade/src/precedence/origin.ts b/packages/alfa-cascade/src/precedence/origin.ts new file mode 100644 index 0000000000..f326a1865c --- /dev/null +++ b/packages/alfa-cascade/src/precedence/origin.ts @@ -0,0 +1,19 @@ +/** + * Cascading origins defined in ascending order; origins defined first have + * lower precedence than origins defined later. + * + * {@link https://www.w3.org/TR/css-cascade-5/#cascading-origins} + * + * @public + */ +export enum Origin { + /** + * {@link https://www.w3.org/TR/css-cascade-5/#cascade-origin-ua} + */ + UserAgent = 1, + + /** + * {@link https://www.w3.org/TR/css-cascade-5/#cascade-origin-author} + */ + Author = 2, +} diff --git a/packages/alfa-cascade/src/precedence/precedence.ts b/packages/alfa-cascade/src/precedence/precedence.ts new file mode 100644 index 0000000000..fca9dfd890 --- /dev/null +++ b/packages/alfa-cascade/src/precedence/precedence.ts @@ -0,0 +1,40 @@ +import type { Comparison } from "@siteimprove/alfa-comparable"; +import type { Specificity } from "@siteimprove/alfa-selector/src/specificity"; +import type { Origin } from "./origin"; + +/** + * Store the varuipous components needed for precedence in the Cascade Sorting Order. + * + * {@link https://drafts.csswg.org/css-cascade-5/#cascade-sort} + * + * @public + */ +export interface Precedence { + origin: Origin; + specificity: Specificity; + order: number; +} + +/** + * @public + */ +export namespace Precedence { + export function comparer(a: Precedence, b: Precedence): Comparison { + // First priority: Origin + if (a.origin !== b.origin) { + return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0; + } + + // Second priority: Specificity. + if (a.specificity.value !== b.specificity.value) { + return a.specificity.value < b.specificity.value + ? -1 + : a.specificity.value > b.specificity.value + ? 1 + : 0; + } + + // Third priority: Order. + return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; + } +} diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 3d401f00ac..77bc7f4db4 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -16,29 +16,22 @@ import { Option } from "@siteimprove/alfa-option"; import { Predicate } from "@siteimprove/alfa-predicate"; import { Refinement } from "@siteimprove/alfa-refinement"; import { - Class, Combinator, Complex, - Compound, Context, - Id, Selector, - Type, } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; -import { UserAgent } from "./user-agent"; import { AncestorFilter } from "./ancestor-filter"; +import { Origin, type Precedence } from "./precedence"; +import { UserAgent } from "./user-agent"; const { equals, property } = Predicate; const { and } = Refinement; -const { isClass } = Class; const { isComplex } = Complex; -const { isCompound } = Compound; -const { isId } = Id; -const { isType } = Type; const isDescendantSelector = and( isComplex, @@ -48,24 +41,6 @@ const isDescendantSelector = and( ), ); -/** - * Cascading origins defined in ascending order; origins defined first have - * lower precedence than origins defined later. - * - * {@link https://www.w3.org/TR/css-cascade-5/#cascading-origins} - */ -enum Origin { - /** - * {@link https://www.w3.org/TR/css-cascade-5/#cascade-origin-ua} - */ - UserAgent = 1, - - /** - * {@link https://www.w3.org/TR/css-cascade-5/#cascade-origin-author} - */ - Author = 2, -} - /** * The selector map is a data structure used for providing indexed access to the * rules that are likely to match a given element. @@ -207,8 +182,7 @@ export namespace SelectorMap { rule: Rule, selector: Selector, declarations: Iterable, - origin: Origin, - order: number, + { origin, order }: Precedence, ): void => { const node = Node.of(rule, selector, declarations, origin, order); @@ -239,7 +213,11 @@ export namespace SelectorMap { order++; for (const part of selector) { - add(rule, part, rule.style, origin, order); + add(rule, part, rule.style, { + origin, + order, + specificity: selector.specificity, + }); } } } diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 28454a283a..36c730091c 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -9,6 +9,9 @@ "src/ancestor-filter.ts", "src/cascade.ts", "src/index.ts", + "src/precedence/index.ts", + "src/precedence/origin.ts", + "src/precedence/precedence.ts", "src/rule-tree.ts", "src/selector-map.ts", "src/user-agent.ts", From a96312b0b5fe7b5d246658a6f2e6169acd7a45e2 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 13:01:15 +0100 Subject: [PATCH 03/26] Start building Precedence interface --- packages/alfa-cascade/src/cascade.ts | 27 +++---------------- .../alfa-cascade/src/precedence/precedence.ts | 24 ++++++++++++----- packages/alfa-cascade/src/selector-map.ts | 3 ++- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index e316123d82..9e61ed5d3d 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -9,6 +9,7 @@ import { Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; +import { Precedence } from "./precedence"; import { RuleTree } from "./rule-tree"; import { SelectorMap } from "./selector-map"; import { UserAgent } from "./user-agent"; @@ -81,7 +82,9 @@ export class Cascade implements Serializable { .get(element, Cache.empty) .get(context, () => this._rules.add( - this._selectors.get(element, context, filter).sort(compare), + this._selectors + .get(element, context, filter) + .sort(Precedence.comparer), ), ); } @@ -108,25 +111,3 @@ export namespace Cascade { rules: RuleTree.JSON; } } - -/** - * {@link https://drafts.csswg.org/css-cascade-5/#cascade-sort} - */ -const compare: Comparer = (a, b) => { - // First priority: Origin - if (a.origin !== b.origin) { - return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0; - } - - // Second priority: Specificity. - if (a.specificity !== b.specificity) { - return a.specificity < b.specificity - ? -1 - : a.specificity > b.specificity - ? 1 - : 0; - } - - // Third priority: Order. - return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; -}; diff --git a/packages/alfa-cascade/src/precedence/precedence.ts b/packages/alfa-cascade/src/precedence/precedence.ts index fca9dfd890..e45b646834 100644 --- a/packages/alfa-cascade/src/precedence/precedence.ts +++ b/packages/alfa-cascade/src/precedence/precedence.ts @@ -1,4 +1,4 @@ -import type { Comparison } from "@siteimprove/alfa-comparable"; +import type { Comparer, Comparison } from "@siteimprove/alfa-comparable"; import type { Specificity } from "@siteimprove/alfa-selector/src/specificity"; import type { Origin } from "./origin"; @@ -11,30 +11,40 @@ import type { Origin } from "./origin"; */ export interface Precedence { origin: Origin; - specificity: Specificity; + // specificity: Specificity; + specificity: number; order: number; } /** + * {@link https://drafts.csswg.org/css-cascade-5/#cascade-sort} + * * @public */ export namespace Precedence { - export function comparer(a: Precedence, b: Precedence): Comparison { + export const comparer: Comparer = (a, b) => { // First priority: Origin if (a.origin !== b.origin) { return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0; } // Second priority: Specificity. - if (a.specificity.value !== b.specificity.value) { - return a.specificity.value < b.specificity.value + // if (a.specificity.value !== b.specificity.value) { + // return a.specificity.value < b.specificity.value + // ? -1 + // : a.specificity.value > b.specificity.value + // ? 1 + // : 0; + // } + if (a.specificity !== b.specificity) { + return a.specificity < b.specificity ? -1 - : a.specificity.value > b.specificity.value + : a.specificity > b.specificity ? 1 : 0; } // Third priority: Order. return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; - } + }; } diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 77bc7f4db4..29965b69d7 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -216,7 +216,8 @@ export namespace SelectorMap { add(rule, part, rule.style, { origin, order, - specificity: selector.specificity, + specificity: selector.specificity.value, + // specificity: selector.specificity, }); } } From e8af2023aebe9e4d9ee675f24aab9c96378cf0fa Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 13:33:07 +0100 Subject: [PATCH 04/26] Add compareLexicographically --- .changeset/pretty-buttons-own.md | 7 +++ docs/review/api/alfa-comparable.api.md | 7 +++ packages/alfa-comparable/package.json | 3 ++ packages/alfa-comparable/src/comparable.ts | 20 ++++++++ packages/alfa-comparable/src/comparer.ts | 10 ++++ .../alfa-comparable/test/comparable.spec.ts | 48 +++++++++++++++++++ packages/alfa-comparable/tsconfig.json | 5 +- 7 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/pretty-buttons-own.md create mode 100644 packages/alfa-comparable/test/comparable.spec.ts diff --git a/.changeset/pretty-buttons-own.md b/.changeset/pretty-buttons-own.md new file mode 100644 index 0000000000..0e9fd1f883 --- /dev/null +++ b/.changeset/pretty-buttons-own.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-comparable": minor +--- + +**Added:** A `Comparable.compareLexicographically` function to compare tuples. + +Both tuples must have the same type, and an adequate tuple of comparer must be provided as a third argument. diff --git a/docs/review/api/alfa-comparable.api.md b/docs/review/api/alfa-comparable.api.md index 19b33ea84c..224b3ddeb5 100644 --- a/docs/review/api/alfa-comparable.api.md +++ b/docs/review/api/alfa-comparable.api.md @@ -22,6 +22,7 @@ export namespace Comparable { export function compareBoolean(a: boolean, b: boolean): Comparison; // (undocumented) export function compareComparable, U = T>(a: T, b: U): Comparison; + export function compareLexicographically>(a: T, b: T, comparer: TupleComparer): Comparison; // (undocumented) export function compareNumber(a: number, b: number): Comparison; // (undocumented) @@ -47,6 +48,12 @@ export enum Comparison { Less = -1 } +// @public +export type TupleComparer> = T extends [ +infer Head, +...infer Tail +] ? [Comparer, ...TupleComparer] : []; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/alfa-comparable/package.json b/packages/alfa-comparable/package.json index d632cfc355..0e749b243d 100644 --- a/packages/alfa-comparable/package.json +++ b/packages/alfa-comparable/package.json @@ -20,6 +20,9 @@ "dependencies": { "@siteimprove/alfa-refinement": "workspace:^0.70.0" }, + "devDependencies": { + "@siteimprove/alfa-test": "workspace:^0.70.0" + }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" diff --git a/packages/alfa-comparable/src/comparable.ts b/packages/alfa-comparable/src/comparable.ts index 0f22e0dc51..c012a24c13 100644 --- a/packages/alfa-comparable/src/comparable.ts +++ b/packages/alfa-comparable/src/comparable.ts @@ -1,4 +1,5 @@ import { Refinement } from "@siteimprove/alfa-refinement"; +import type { Comparer, TupleComparer } from "./comparer"; import { Comparison } from "./comparison"; @@ -173,6 +174,25 @@ export namespace Comparable { return Comparison.Equal; } + /** + * Compare tuples lexicographically + */ + export function compareLexicographically>( + a: T, + b: T, + comparer: TupleComparer, + ): Comparison { + for (let i = 0; i < a.length; i++) { + const comparison = (comparer[i] as Comparer)(a[i], b[i]); + if (comparison === Comparison.Equal) { + continue; + } + return comparison; + } + + return Comparison.Equal; + } + /** * Check if one value is less than another. */ diff --git a/packages/alfa-comparable/src/comparer.ts b/packages/alfa-comparable/src/comparer.ts index 5ecce1fcdf..8b52e377bb 100644 --- a/packages/alfa-comparable/src/comparer.ts +++ b/packages/alfa-comparable/src/comparer.ts @@ -8,3 +8,13 @@ export type Comparer = []> = ( b: U, ...args: A ) => Comparison; + +/** + * Turns a tuple of types into a tuple of comparer over these types. + */ +export type TupleComparer> = T extends [ + infer Head, + ...infer Tail, +] + ? [Comparer, ...TupleComparer] + : []; diff --git a/packages/alfa-comparable/test/comparable.spec.ts b/packages/alfa-comparable/test/comparable.spec.ts new file mode 100644 index 0000000000..b712ac379f --- /dev/null +++ b/packages/alfa-comparable/test/comparable.spec.ts @@ -0,0 +1,48 @@ +import { type RNG, test } from "@siteimprove/alfa-test"; +import { Comparable, Comparison } from "../src"; + +function wrapper(rng: RNG): RNG<[number, number]> { + return () => [Math.round(rng() * 1000), Math.round(rng() * 1000)]; +} + +test( + "compareLexicographically compares couple of integers", + (t, rng, seed) => { + const [a, b] = rng(); + const [c, d] = rng(); + + t.deepEqual( + Comparable.compareLexicographically<[number, number]>( + [a, b], + [c, d], + [Comparable.compareNumber, Comparable.compareNumber], + ), + a < c + ? Comparison.Less + : a > c + ? Comparison.Greater + : b < d + ? Comparison.Less + : b > d + ? Comparison.Greater + : Comparison.Equal, + `Failing lexicographic comparison of [${a}, ${b}] and [${c}, ${d}] at seed ${seed}`, + ); + }, + { wrapper, iterations: 100 }, +); + +test("compareLexicographically compares heterogenous tuples", (t) => { + const a: [number, string, string] = [1, "a", "a"]; + // nearly equal to force comparsion go all the way. + const b: [number, string, string] = [1, "a", "b"]; + + t.deepEqual( + Comparable.compareLexicographically(a, b, [ + Comparable.compareNumber, + Comparable.compareString, + Comparable.compareString, + ]), + Comparison.Less, + ); +}); diff --git a/packages/alfa-comparable/tsconfig.json b/packages/alfa-comparable/tsconfig.json index e62ce27bd5..543516ef3f 100644 --- a/packages/alfa-comparable/tsconfig.json +++ b/packages/alfa-comparable/tsconfig.json @@ -5,7 +5,8 @@ "src/comparable.ts", "src/comparer.ts", "src/comparison.ts", - "src/index.ts" + "src/index.ts", + "test/comparable.spec.ts" ], - "references": [{ "path": "../alfa-refinement" }] + "references": [{ "path": "../alfa-refinement" }, { "path": "../alfa-test" }] } From 096fdfb755557c805628f4dbd6a3026452c8398d Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 14:54:56 +0100 Subject: [PATCH 05/26] Streamline cascade sorting order comparisons --- .changeset/weak-rockets-add.md | 5 +++ .../alfa-cascade/src/precedence/origin.ts | 9 ++++ .../alfa-cascade/src/precedence/precedence.ts | 42 +++++-------------- packages/alfa-cascade/src/selector-map.ts | 15 +++---- packages/alfa-comparable/src/comparable.ts | 2 + packages/alfa-selector/package.json | 1 + packages/alfa-selector/src/index.ts | 1 + packages/alfa-selector/src/specificity.ts | 14 ++++--- packages/alfa-selector/tsconfig.json | 1 + 9 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 .changeset/weak-rockets-add.md diff --git a/.changeset/weak-rockets-add.md b/.changeset/weak-rockets-add.md new file mode 100644 index 0000000000..682bf338d1 --- /dev/null +++ b/.changeset/weak-rockets-add.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-selector": minor +--- + +**Added:** `Spoecificity` is now exported for external use. diff --git a/packages/alfa-cascade/src/precedence/origin.ts b/packages/alfa-cascade/src/precedence/origin.ts index f326a1865c..fc8e31f260 100644 --- a/packages/alfa-cascade/src/precedence/origin.ts +++ b/packages/alfa-cascade/src/precedence/origin.ts @@ -1,3 +1,5 @@ +import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; + /** * Cascading origins defined in ascending order; origins defined first have * lower precedence than origins defined later. @@ -17,3 +19,10 @@ export enum Origin { */ Author = 2, } + +/** + * @public + */ +export namespace Origin { + export const compare: Comparer = Comparable.compareNumber; +} diff --git a/packages/alfa-cascade/src/precedence/precedence.ts b/packages/alfa-cascade/src/precedence/precedence.ts index e45b646834..d9dc8ff8d3 100644 --- a/packages/alfa-cascade/src/precedence/precedence.ts +++ b/packages/alfa-cascade/src/precedence/precedence.ts @@ -1,9 +1,9 @@ -import type { Comparer, Comparison } from "@siteimprove/alfa-comparable"; -import type { Specificity } from "@siteimprove/alfa-selector/src/specificity"; -import type { Origin } from "./origin"; +import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; +import { Specificity } from "@siteimprove/alfa-selector/src/specificity"; +import { Origin } from "./origin"; /** - * Store the varuipous components needed for precedence in the Cascade Sorting Order. + * Store the various components needed for precedence in the Cascade Sorting Order. * * {@link https://drafts.csswg.org/css-cascade-5/#cascade-sort} * @@ -11,8 +11,7 @@ import type { Origin } from "./origin"; */ export interface Precedence { origin: Origin; - // specificity: Specificity; - specificity: number; + specificity: Specificity; order: number; } @@ -22,29 +21,10 @@ export interface Precedence { * @public */ export namespace Precedence { - export const comparer: Comparer = (a, b) => { - // First priority: Origin - if (a.origin !== b.origin) { - return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0; - } - - // Second priority: Specificity. - // if (a.specificity.value !== b.specificity.value) { - // return a.specificity.value < b.specificity.value - // ? -1 - // : a.specificity.value > b.specificity.value - // ? 1 - // : 0; - // } - if (a.specificity !== b.specificity) { - return a.specificity < b.specificity - ? -1 - : a.specificity > b.specificity - ? 1 - : 0; - } - - // Third priority: Order. - return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; - }; + export const comparer: Comparer = (a, b) => + Comparable.compareLexicographically<[Origin, Specificity, number]>( + [a.origin, a.specificity, a.order], + [b.origin, b.specificity, b.order], + [Origin.compare, Specificity.compare, Comparable.compareNumber], + ); } diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 29965b69d7..60f0e51d16 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -20,6 +20,7 @@ import { Complex, Context, Selector, + Specificity, } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; @@ -216,7 +217,7 @@ export namespace SelectorMap { add(rule, part, rule.style, { origin, order, - specificity: selector.specificity.value, + specificity: selector.specificity, // specificity: selector.specificity, }); } @@ -293,7 +294,7 @@ export namespace SelectorMap { private readonly _declarations: Iterable; private readonly _origin: Origin; private readonly _order: number; - private readonly _specificity: number; + private readonly _specificity: Specificity; private constructor( rule: Rule, @@ -313,8 +314,8 @@ export namespace SelectorMap { // Otherwise, use the specificity of the selector. this._specificity = StyleRule.isStyleRule(rule) && rule.hint - ? 0 - : selector.specificity.value; + ? Specificity.empty() + : selector.specificity; } public get rule(): Rule { @@ -337,7 +338,7 @@ export namespace SelectorMap { return this._order; } - public get specificity(): number { + public get specificity(): Specificity { return this._specificity; } @@ -350,7 +351,7 @@ export namespace SelectorMap { ), origin: this._origin, order: this._order, - specificity: this._specificity, + specificity: this._specificity.toJSON(), }; } } @@ -363,7 +364,7 @@ export namespace SelectorMap { declarations: Array; origin: Origin; order: number; - specificity: number; + specificity: Specificity.JSON; } } diff --git a/packages/alfa-comparable/src/comparable.ts b/packages/alfa-comparable/src/comparable.ts index c012a24c13..e3f0a653f7 100644 --- a/packages/alfa-comparable/src/comparable.ts +++ b/packages/alfa-comparable/src/comparable.ts @@ -176,6 +176,8 @@ export namespace Comparable { /** * Compare tuples lexicographically + * + * {@link https://en.wikipedia.org/wiki/Lexicographic_order} */ export function compareLexicographically>( a: T, diff --git a/packages/alfa-selector/package.json b/packages/alfa-selector/package.json index 16ef8c18d9..419b723532 100644 --- a/packages/alfa-selector/package.json +++ b/packages/alfa-selector/package.json @@ -20,6 +20,7 @@ "dependencies": { "@siteimprove/alfa-array": "workspace:^0.70.0", "@siteimprove/alfa-cache": "workspace:^0.70.0", + "@siteimprove/alfa-comparable": "workspace:^0.70.0", "@siteimprove/alfa-css": "workspace:^0.70.0", "@siteimprove/alfa-dom": "workspace:^0.70.0", "@siteimprove/alfa-equatable": "workspace:^0.70.0", diff --git a/packages/alfa-selector/src/index.ts b/packages/alfa-selector/src/index.ts index 974105ff6f..a500ba3a28 100644 --- a/packages/alfa-selector/src/index.ts +++ b/packages/alfa-selector/src/index.ts @@ -1,2 +1,3 @@ export * from "./context"; export * from "./selector"; +export * from "./specificity"; diff --git a/packages/alfa-selector/src/specificity.ts b/packages/alfa-selector/src/specificity.ts index 9b071e903e..5a994e7a21 100644 --- a/packages/alfa-selector/src/specificity.ts +++ b/packages/alfa-selector/src/specificity.ts @@ -1,3 +1,4 @@ +import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; import { Equatable } from "@siteimprove/alfa-equatable"; import { Hash, Hashable } from "@siteimprove/alfa-hash"; import { Serializable } from "@siteimprove/alfa-json"; @@ -24,12 +25,7 @@ const componentMax = (1 << componentBits) - 1; * comparison, which is the frequent operation on specificities. Components are * therefore limited to 1024 values (10 bits). * - * @privateRemarks - * This class purposefully doesn't implement the Comparable interface. The - * interface introduce a tiny overhead in converting the native comparison into - * the Comparison enum. While this price is gladly payed for more readable code - * in general, specificity comparison is a hot-path in cascading style which - * we try to keep as efficient as possible. + * @public */ export class Specificity implements Serializable, Equatable, Hashable @@ -97,6 +93,9 @@ export class Specificity } } +/** + * @public + */ export namespace Specificity { export interface JSON { [key: string]: json.JSON; @@ -138,4 +137,7 @@ export namespace Specificity { first, ); } + + export const compare: Comparer = (a, b) => + Comparable.compareNumber(a.value, b.value); } diff --git a/packages/alfa-selector/tsconfig.json b/packages/alfa-selector/tsconfig.json index 5caa0157e3..1f354a00bb 100644 --- a/packages/alfa-selector/tsconfig.json +++ b/packages/alfa-selector/tsconfig.json @@ -82,6 +82,7 @@ "references": [ { "path": "../alfa-array" }, { "path": "../alfa-cache" }, + { "path": "../alfa-comparable" }, { "path": "../alfa-css" }, { "path": "../alfa-dom" }, { "path": "../alfa-equatable" }, From db1c600146111863fb822cbc9bb41448bf32d956 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 15:00:47 +0100 Subject: [PATCH 06/26] Wrap Order into its own type (alias for number) --- packages/alfa-cascade/src/precedence/index.ts | 1 + packages/alfa-cascade/src/precedence/order.ts | 21 +++++++++++++++++++ .../alfa-cascade/src/precedence/origin.ts | 2 ++ .../alfa-cascade/src/precedence/precedence.ts | 7 ++++--- packages/alfa-cascade/src/selector-map.ts | 16 +++++++------- packages/alfa-cascade/tsconfig.json | 1 + 6 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 packages/alfa-cascade/src/precedence/order.ts diff --git a/packages/alfa-cascade/src/precedence/index.ts b/packages/alfa-cascade/src/precedence/index.ts index 65d43a10ff..f69b9a3f0e 100644 --- a/packages/alfa-cascade/src/precedence/index.ts +++ b/packages/alfa-cascade/src/precedence/index.ts @@ -1,2 +1,3 @@ +export * from "./order"; export * from "./origin"; export * from "./precedence"; diff --git a/packages/alfa-cascade/src/precedence/order.ts b/packages/alfa-cascade/src/precedence/order.ts new file mode 100644 index 0000000000..8d3d54d579 --- /dev/null +++ b/packages/alfa-cascade/src/precedence/order.ts @@ -0,0 +1,21 @@ +import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; + +/** + * Order of appearance of CSS rules in a sheet. + * + * @privateRemarks + * While this just wraps `number`, having it as its own type streamlines + * the rest of the code. + * + * @public + */ +export type Order = number; + +/** + * @public + */ +export namespace Order { + export type JSON = Order; + + export const compare: Comparer = Comparable.compareNumber; +} diff --git a/packages/alfa-cascade/src/precedence/origin.ts b/packages/alfa-cascade/src/precedence/origin.ts index fc8e31f260..a4c94fa2a9 100644 --- a/packages/alfa-cascade/src/precedence/origin.ts +++ b/packages/alfa-cascade/src/precedence/origin.ts @@ -24,5 +24,7 @@ export enum Origin { * @public */ export namespace Origin { + export type JSON = Origin; + export const compare: Comparer = Comparable.compareNumber; } diff --git a/packages/alfa-cascade/src/precedence/precedence.ts b/packages/alfa-cascade/src/precedence/precedence.ts index d9dc8ff8d3..45c7483b41 100644 --- a/packages/alfa-cascade/src/precedence/precedence.ts +++ b/packages/alfa-cascade/src/precedence/precedence.ts @@ -1,5 +1,6 @@ import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; import { Specificity } from "@siteimprove/alfa-selector/src/specificity"; +import { Order } from "./order"; import { Origin } from "./origin"; /** @@ -12,7 +13,7 @@ import { Origin } from "./origin"; export interface Precedence { origin: Origin; specificity: Specificity; - order: number; + order: Order; } /** @@ -22,9 +23,9 @@ export interface Precedence { */ export namespace Precedence { export const comparer: Comparer = (a, b) => - Comparable.compareLexicographically<[Origin, Specificity, number]>( + Comparable.compareLexicographically<[Origin, Specificity, Order]>( [a.origin, a.specificity, a.order], [b.origin, b.specificity, b.order], - [Origin.compare, Specificity.compare, Comparable.compareNumber], + [Origin.compare, Specificity.compare, Order.compare], ); } diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 60f0e51d16..9a1d70889b 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -26,7 +26,7 @@ import { import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; -import { Origin, type Precedence } from "./precedence"; +import { type Order, Origin, type Precedence } from "./precedence"; import { UserAgent } from "./user-agent"; const { equals, property } = Predicate; @@ -172,7 +172,7 @@ export namespace SelectorMap { // order in which they were declared, information related to ordering will // otherwise no longer be available once rules from different buckets are // combined. - let order = 0; + let order: Order = 0; const ids = Bucket.empty(); const classes = Bucket.empty(); @@ -284,7 +284,7 @@ export namespace SelectorMap { selector: Selector, declarations: Iterable, origin: Origin, - order: number, + order: Order, ): Node { return new Node(rule, selector, declarations, origin, order); } @@ -293,7 +293,7 @@ export namespace SelectorMap { private readonly _selector: Selector; private readonly _declarations: Iterable; private readonly _origin: Origin; - private readonly _order: number; + private readonly _order: Order; private readonly _specificity: Specificity; private constructor( @@ -301,7 +301,7 @@ export namespace SelectorMap { selector: Selector, declarations: Iterable, origin: Origin, - order: number, + order: Order, ) { this._rule = rule; this._selector = selector; @@ -334,7 +334,7 @@ export namespace SelectorMap { return this._origin; } - public get order(): number { + public get order(): Order { return this._order; } @@ -362,8 +362,8 @@ export namespace SelectorMap { rule: Rule.JSON; selector: Selector.JSON; declarations: Array; - origin: Origin; - order: number; + origin: Origin.JSON; + order: Order.JSON; specificity: Specificity.JSON; } } diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 36c730091c..ee26c11cb3 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -10,6 +10,7 @@ "src/cascade.ts", "src/index.ts", "src/precedence/index.ts", + "src/precedence/order.ts", "src/precedence/origin.ts", "src/precedence/precedence.ts", "src/rule-tree.ts", From 7beef8e1ecfacd53c3e7f73fc7e4719630fc79ce Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 15 Dec 2023 15:59:34 +0100 Subject: [PATCH 07/26] Share block structure between rule tree and selector map --- packages/alfa-cascade/src/block.ts | 115 +++++++++ packages/alfa-cascade/src/cascade.ts | 6 +- .../alfa-cascade/src/precedence/precedence.ts | 19 +- packages/alfa-cascade/src/rule-tree.ts | 96 +++----- packages/alfa-cascade/src/selector-map.ts | 221 +++++++++--------- packages/alfa-cascade/test/rule-tree.spec.ts | 145 ++++++------ packages/alfa-cascade/tsconfig.json | 1 + packages/alfa-rules/src/sia-r83/rule.ts | 2 +- packages/alfa-style/src/style.ts | 2 +- 9 files changed, 355 insertions(+), 252 deletions(-) create mode 100644 packages/alfa-cascade/src/block.ts diff --git a/packages/alfa-cascade/src/block.ts b/packages/alfa-cascade/src/block.ts new file mode 100644 index 0000000000..edabebdf95 --- /dev/null +++ b/packages/alfa-cascade/src/block.ts @@ -0,0 +1,115 @@ +import { Array } from "@siteimprove/alfa-array"; +import { type Comparer, Comparison } from "@siteimprove/alfa-comparable"; +import { Declaration, Rule, StyleRule } from "@siteimprove/alfa-dom"; +import type { Equatable } from "@siteimprove/alfa-equatable"; +import { Iterable } from "@siteimprove/alfa-iterable"; +import type { Serializable } from "@siteimprove/alfa-json"; +import { Selector } from "@siteimprove/alfa-selector"; + +import * as json from "@siteimprove/alfa-json"; + +import { Precedence } from "./precedence"; + +/** + * While resolving cascade, a Block is a style rule that has been expanded with + * its selector pre-parsed, its declarations extracted, and extra information + * about Cascade Sorting Precedence. + * + * @remarks + * Blocks imply coupling between the different parts and are thus grouped into + * a single structure. + * + * Blocks form the data stored in the rule tree and selector map. Upon building the + * cascade, style rules are turned into Blocks which are inserted into the selector + * map; and then relevant Blocks are inserted into the rule tree upon matching elements. + * + * @internal + */ +export class Block implements Equatable, Serializable { + /** + * Create a block. + * + * @remarks + * This does not validate coupling of the data. Prefer using Block.from() + */ + public static of( + rule: StyleRule, + selector: Selector, + declarations: Iterable, + precedence: Precedence, + ): Block { + return new Block(rule, selector, Array.from(declarations), precedence); + } + + private readonly _rule: StyleRule; + private readonly _selector: Selector; + private readonly _declarations: Array; + private readonly _precedence: Precedence; + + constructor( + rule: StyleRule, + selector: Selector, + declarations: Array, + precedence: Precedence, + ) { + this._rule = rule; + this._selector = selector; + this._declarations = declarations; + this._precedence = precedence; + } + + public get rule(): StyleRule { + return this._rule; + } + + public get selector(): Selector { + return this._selector; + } + + public get declarations(): Iterable { + return this._declarations; + } + + public get precedence(): Readonly { + return this._precedence; + } + + public equals(value: Block): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof Block && + this._rule.equals(value._rule) && + this._selector.equals(value._selector) && + Array.equals(value._declarations, this._declarations) && + Precedence.compare(value._precedence, this._precedence) === + Comparison.Equal + ); + } + + public toJSON(): Block.JSON { + return { + rule: this._rule.toJSON(), + selector: this._selector.toJSON(), + declarations: Array.toJSON(this._declarations), + precedence: Precedence.toJSON(this._precedence), + }; + } +} +/** + * @internal + */ +export namespace Block { + export interface JSON { + [key: string]: json.JSON; + rule: Rule.JSON; + selector: Selector.JSON; + declarations: Array; + precedence: Precedence.JSON; + } + + export const compare: Comparer = (a, b) => + Precedence.compare(a.precedence, b.precedence); +} diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 9e61ed5d3d..765cc977d4 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -1,5 +1,4 @@ import { Cache } from "@siteimprove/alfa-cache"; -import { Comparer } from "@siteimprove/alfa-comparable"; import { Device } from "@siteimprove/alfa-device"; import { Document, Element, Node, Shadow } from "@siteimprove/alfa-dom"; import { Serializable } from "@siteimprove/alfa-json"; @@ -9,6 +8,7 @@ import { Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; +import { Block } from "./block"; import { Precedence } from "./precedence"; import { RuleTree } from "./rule-tree"; import { SelectorMap } from "./selector-map"; @@ -82,9 +82,7 @@ export class Cascade implements Serializable { .get(element, Cache.empty) .get(context, () => this._rules.add( - this._selectors - .get(element, context, filter) - .sort(Precedence.comparer), + this._selectors.get(element, context, filter).sort(Block.compare), ), ); } diff --git a/packages/alfa-cascade/src/precedence/precedence.ts b/packages/alfa-cascade/src/precedence/precedence.ts index 45c7483b41..cf88f18a63 100644 --- a/packages/alfa-cascade/src/precedence/precedence.ts +++ b/packages/alfa-cascade/src/precedence/precedence.ts @@ -1,5 +1,8 @@ import { Comparable, type Comparer } from "@siteimprove/alfa-comparable"; import { Specificity } from "@siteimprove/alfa-selector/src/specificity"; + +import * as json from "@siteimprove/alfa-json"; + import { Order } from "./order"; import { Origin } from "./origin"; @@ -22,7 +25,21 @@ export interface Precedence { * @public */ export namespace Precedence { - export const comparer: Comparer = (a, b) => + export interface JSON { + [key: string]: json.JSON; + origin: Origin.JSON; + specificity: Specificity.JSON; + order: Order.JSON; + } + + export function toJSON(precedence: Precedence): JSON { + return { + origin: precedence.origin, + specificity: precedence.specificity.toJSON(), + order: precedence.order, + }; + } + export const compare: Comparer = (a, b) => Comparable.compareLexicographically<[Origin, Specificity, Order]>( [a.origin, a.specificity, a.order], [b.origin, b.specificity, b.order], diff --git a/packages/alfa-cascade/src/rule-tree.ts b/packages/alfa-cascade/src/rule-tree.ts index ec2592e7a9..c4be763daf 100644 --- a/packages/alfa-cascade/src/rule-tree.ts +++ b/packages/alfa-cascade/src/rule-tree.ts @@ -1,10 +1,12 @@ import { Declaration, h, Rule } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; +import * as json from "@siteimprove/alfa-json"; import { Serializable } from "@siteimprove/alfa-json"; import { None, Option } from "@siteimprove/alfa-option"; -import { Selector, Universal } from "@siteimprove/alfa-selector"; +import { Selector, Specificity, Universal } from "@siteimprove/alfa-selector"; -import * as json from "@siteimprove/alfa-json"; +import { Block } from "./block"; +import { Origin } from "./precedence"; /** * The rule tree is a data structure used for storing the rules that match each @@ -80,11 +82,11 @@ export class RuleTree implements Serializable { // Rooting the forest at a fake node with no declaration. private readonly _root: RuleTree.Node = RuleTree.Node.of( - { - rule: h.rule.style("*", []), - selector: Universal.of(None), - declarations: [], - }, + Block.of(h.rule.style("*", []), Universal.of(None), [], { + origin: Origin.UserAgent, + specificity: Specificity.empty(), + order: Infinity, + }), [], None, ); @@ -111,15 +113,15 @@ export class RuleTree implements Serializable { * * @internal */ - public add(rules: Iterable): RuleTree.Node { + public add(rules: Iterable): RuleTree.Node { let parent = this._root; - for (const item of rules) { + for (const block of rules) { // Insert the next rule into the current parent, using the returned rule // entry as the parent of the next rule to insert. This way, we gradually // build up a path of rule entries and then return the final entry to the // caller. - parent = parent.add(item); + parent = parent.add(block); } return parent; @@ -136,71 +138,31 @@ export class RuleTree implements Serializable { export namespace RuleTree { export type JSON = Array; - /** - * Items stored in rule tree nodes. - * - * @remarks - * Only the selector is used to actually build the structure. The rule and - * declarations are just data passed along to be used when resolving style. - * - * If the selector does not match the one in the rule, behavior is not specified. - * - * @internal - */ - export interface Item { - rule: Rule; - selector: Selector; - declarations: Iterable; - } - - export namespace Item { - export interface JSON { - [key: string]: json.JSON; - rule: Rule.JSON; - selector: Selector.JSON; - declarations: Array; - } - } - export class Node implements Serializable { public static of( - { rule, selector, declarations }: Item, + block: Block, children: Array, parent: Option, ): Node { - return new Node(rule, selector, declarations, children, parent); + return new Node(block, children, parent); } - private readonly _rule: Rule; - private readonly _selector: Selector; - private readonly _declarations: Iterable; + private readonly _block: Block; private readonly _children: Array; private readonly _parent: Option; private constructor( - rule: Rule, - selector: Selector, - declarations: Iterable, + block: Block, children: Array, parent: Option, ) { - this._rule = rule; - this._selector = selector; - this._declarations = declarations; + this._block = block; this._children = children; this._parent = parent; } - public get rule(): Rule { - return this._rule; - } - - public get selector(): Selector { - return this._selector; - } - - public get declarations(): Iterable { - return this._declarations; + public get block(): Block { + return this._block; } public get children(): Array { @@ -232,7 +194,7 @@ export namespace RuleTree { * * @internal */ - public add(item: Item): Node { + public add(block: Block): Node { // If we have already encountered the exact same selector (physical identity), // we're done. // This occurs when the exact same style rule matches several elements. @@ -241,7 +203,7 @@ export namespace RuleTree { // completely been shared). // Notably, because it is the exact same selector, it controls the exact // same rules, so all the information is already in the tree. - if (this._selector === item.selector) { + if (this._block.selector === block.selector) { return this; } @@ -251,15 +213,15 @@ export namespace RuleTree { // then sorted by order of appearance (by assumption) and the later must // be a descendant of the former as it has higher precedence. for (const child of this._children) { - if (child._selector.equals(item.selector)) { - return child.add(item); + if (child._block.selector.equals(block.selector)) { + return child.add(block); } } // Otherwise, the selector is brand new (for this branch of the tree). // Add it as a new child and return it (further rules in the same batch, // matching the same element, should be added as its child. - const node = Node.of(item, [], Option.of(this)); + const node = Node.of(block, [], Option.of(this)); this._children.push(node); @@ -268,13 +230,7 @@ export namespace RuleTree { public toJSON(): Node.JSON { return { - item: { - rule: this._rule.toJSON(), - selector: this._selector.toJSON(), - declarations: [...this._declarations].map((declaration) => - declaration.toJSON(), - ), - }, + block: this._block.toJSON(), children: this._children.map((node) => node.toJSON()), }; } @@ -283,7 +239,7 @@ export namespace RuleTree { export namespace Node { export interface JSON { [key: string]: json.JSON; - item: Item.JSON; + block: Block.JSON; children: Array; } } diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 9a1d70889b..9b32c14157 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -1,3 +1,4 @@ +import { Array } from "@siteimprove/alfa-array"; import { Lexer } from "@siteimprove/alfa-css"; import { Device } from "@siteimprove/alfa-device"; import { @@ -26,6 +27,7 @@ import { import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; +import { Block } from "./block"; import { type Order, Origin, type Precedence } from "./precedence"; import { UserAgent } from "./user-agent"; @@ -82,7 +84,7 @@ export class SelectorMap implements Serializable { ids: SelectorMap.Bucket, classes: SelectorMap.Bucket, types: SelectorMap.Bucket, - other: Array, + other: Array, ): SelectorMap { return new SelectorMap(ids, classes, types, other); } @@ -90,13 +92,13 @@ export class SelectorMap implements Serializable { private readonly _ids: SelectorMap.Bucket; private readonly _classes: SelectorMap.Bucket; private readonly _types: SelectorMap.Bucket; - private readonly _other: Array; + private readonly _other: Array; private constructor( ids: SelectorMap.Bucket, classes: SelectorMap.Bucket, types: SelectorMap.Bucket, - other: Array, + other: Array, ) { this._ids = ids; this._classes = classes; @@ -108,10 +110,10 @@ export class SelectorMap implements Serializable { element: Element, context: Context, filter: Option, - ): Array { - const nodes: Array = []; + ): Array { + const nodes: Array = []; - const collect = (candidates: Iterable) => { + const collect = (candidates: Iterable) => { for (const node of candidates) { if ( filter.none((filter) => @@ -163,7 +165,7 @@ export namespace SelectorMap { ids: Bucket.JSON; classes: Bucket.JSON; types: Bucket.JSON; - other: Array; + other: Array; } export function from(sheets: Iterable, device: Device): SelectorMap { @@ -177,24 +179,31 @@ export namespace SelectorMap { const ids = Bucket.empty(); const classes = Bucket.empty(); const types = Bucket.empty(); - const other: Array = []; + const other: Array = []; const add = ( - rule: Rule, + rule: StyleRule, selector: Selector, declarations: Iterable, { origin, order }: Precedence, ): void => { - const node = Node.of(rule, selector, declarations, origin, order); + const block = Block.of(rule, selector, Array.from(declarations), { + origin, + order, + specificity: + StyleRule.isStyleRule(rule) && rule.hint + ? Specificity.empty() + : selector.specificity, + }); const keySelector = selector.key; if (!keySelector.isSome()) { - other.push(node); + other.push(block); } else { const key = keySelector.get(); const buckets = { id: ids, class: classes, type: types }; - buckets[key.type].add(key.name, node); + buckets[key.type].add(key.name, block); } }; @@ -278,108 +287,108 @@ export namespace SelectorMap { return SelectorMap.of(ids, classes, types, other); } - export class Node implements Serializable { - public static of( - rule: Rule, - selector: Selector, - declarations: Iterable, - origin: Origin, - order: Order, - ): Node { - return new Node(rule, selector, declarations, origin, order); - } - - private readonly _rule: Rule; - private readonly _selector: Selector; - private readonly _declarations: Iterable; - private readonly _origin: Origin; - private readonly _order: Order; - private readonly _specificity: Specificity; - - private constructor( - rule: Rule, - selector: Selector, - declarations: Iterable, - origin: Origin, - order: Order, - ) { - this._rule = rule; - this._selector = selector; - this._declarations = declarations; - this._origin = origin; - this._order = order; - - // For style rules that are presentational hints, the specificity will - // always be 0 regardless of the selector. - // Otherwise, use the specificity of the selector. - this._specificity = - StyleRule.isStyleRule(rule) && rule.hint - ? Specificity.empty() - : selector.specificity; - } - - public get rule(): Rule { - return this._rule; - } - - public get selector(): Selector { - return this._selector; - } - - public get declarations(): Iterable { - return this._declarations; - } - - public get origin(): Origin { - return this._origin; - } - - public get order(): Order { - return this._order; - } - - public get specificity(): Specificity { - return this._specificity; - } - - public toJSON(): Node.JSON { - return { - rule: this._rule.toJSON(), - selector: this._selector.toJSON(), - declarations: [...this._declarations].map((declaration) => - declaration.toJSON(), - ), - origin: this._origin, - order: this._order, - specificity: this._specificity.toJSON(), - }; - } - } - - export namespace Node { - export interface JSON { - [key: string]: json.JSON; - rule: Rule.JSON; - selector: Selector.JSON; - declarations: Array; - origin: Origin.JSON; - order: Order.JSON; - specificity: Specificity.JSON; - } - } + // export class Node implements Serializable { + // public static of( + // rule: Rule, + // selector: Selector, + // declarations: Iterable, + // origin: Origin, + // order: Order, + // ): Node { + // return new Node(rule, selector, declarations, origin, order); + // } + // + // private readonly _rule: Rule; + // private readonly _selector: Selector; + // private readonly _declarations: Iterable; + // private readonly _origin: Origin; + // private readonly _order: Order; + // private readonly _specificity: Specificity; + // + // private constructor( + // rule: Rule, + // selector: Selector, + // declarations: Iterable, + // origin: Origin, + // order: Order, + // ) { + // this._rule = rule; + // this._selector = selector; + // this._declarations = declarations; + // this._origin = origin; + // this._order = order; + // + // // For style rules that are presentational hints, the specificity will + // // always be 0 regardless of the selector. + // // Otherwise, use the specificity of the selector. + // this._specificity = + // StyleRule.isStyleRule(rule) && rule.hint + // ? Specificity.empty() + // : selector.specificity; + // } + // + // public get rule(): Rule { + // return this._rule; + // } + // + // public get selector(): Selector { + // return this._selector; + // } + // + // public get declarations(): Iterable { + // return this._declarations; + // } + // + // public get origin(): Origin { + // return this._origin; + // } + // + // public get order(): Order { + // return this._order; + // } + // + // public get specificity(): Specificity { + // return this._specificity; + // } + // + // public toJSON(): Node.JSON { + // return { + // rule: this._rule.toJSON(), + // selector: this._selector.toJSON(), + // declarations: [...this._declarations].map((declaration) => + // declaration.toJSON(), + // ), + // origin: this._origin, + // order: this._order, + // specificity: this._specificity.toJSON(), + // }; + // } + // } + // + // export namespace Node { + // export interface JSON { + // [key: string]: json.JSON; + // rule: Rule.JSON; + // selector: Selector.JSON; + // declarations: Array; + // origin: Origin.JSON; + // order: Order.JSON; + // specificity: Specificity.JSON; + // } + // } export class Bucket implements Serializable { public static empty(): Bucket { return new Bucket(new Map()); } - private readonly _nodes: Map>; + private readonly _nodes: Map>; - private constructor(nodes: Map>) { + private constructor(nodes: Map>) { this._nodes = nodes; } - public add(key: string, node: SelectorMap.Node): void { + public add(key: string, node: Block): void { const nodes = this._nodes.get(key); if (nodes === undefined) { @@ -389,7 +398,7 @@ export namespace SelectorMap { } } - public get(key: string): Array { + public get(key: string): Array { const nodes = this._nodes.get(key); if (nodes === undefined) { @@ -408,6 +417,6 @@ export namespace SelectorMap { } export namespace Bucket { - export type JSON = Array<[string, Array]>; + export type JSON = Array<[string, Array]>; } } diff --git a/packages/alfa-cascade/test/rule-tree.spec.ts b/packages/alfa-cascade/test/rule-tree.spec.ts index 87dec15065..10030358ea 100644 --- a/packages/alfa-cascade/test/rule-tree.spec.ts +++ b/packages/alfa-cascade/test/rule-tree.spec.ts @@ -1,26 +1,30 @@ import { h } from "@siteimprove/alfa-dom"; import { None } from "@siteimprove/alfa-option"; -import { test } from "@siteimprove/alfa-test"; +import { Specificity } from "@siteimprove/alfa-selector"; import { parse } from "@siteimprove/alfa-selector/test/parser"; - +import { test } from "@siteimprove/alfa-test"; import { RuleTree } from "../src"; -function fakeItem(selector: string): RuleTree.Item { - return { - rule: h.rule.style(selector, []), - selector: parse(selector), - declarations: [], - }; +import { Block } from "../src/block"; +import { Origin } from "../src/precedence"; + +function fakeBlock(selector: string): Block { + return Block.of(h.rule.style(selector, []), parse(selector), [], { + origin: Origin.UserAgent, + specificity: Specificity.empty(), + order: -1, + }); } -function fakeJSON(selector: string): RuleTree.Item.JSON { - const item = fakeItem(selector); +function fakeJSON(selector: string): Block.JSON { + const item = fakeBlock(selector); return { rule: item.rule.toJSON(), selector: item.selector.toJSON(), declarations: [], + precedence: { origin: 1, specificity: { a: 0, b: 0, c: 0 }, order: -1 }, }; } @@ -28,48 +32,51 @@ function fakeJSON(selector: string): RuleTree.Item.JSON { * Node tests */ test(".of() builds a node", (t) => { - const node = RuleTree.Node.of(fakeItem("div"), [], None); + const node = RuleTree.Node.of(fakeBlock("div"), [], None); t.deepEqual(node.toJSON(), { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [], }); }); test(".add() doesn't change a tree that already has the exact same selector", (t) => { - const item1 = fakeItem("div"); - const item2 = fakeItem("div"); + const item1 = fakeBlock("div"); + const item2 = fakeBlock("div"); const node = RuleTree.Node.of(item1, [], None); - node.add({ ...item2, selector: item1.selector }); + // This is not a mistake, we want to share the exact same selector but have otherwise different parts. + node.add( + Block.of(item2.rule, item1.selector, item2.declarations, item2.precedence), + ); t.deepEqual(node.toJSON(), { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [], }); }); test(".add() adds a child upon inserting identical selector", (t) => { - const node = RuleTree.Node.of(fakeItem("div"), [], None); - node.add(fakeItem("div")); + const node = RuleTree.Node.of(fakeBlock("div"), [], None); + node.add(fakeBlock("div")); t.deepEqual(node.toJSON(), { - item: fakeJSON("div"), - children: [{ item: fakeJSON("div"), children: [] }], + block: fakeJSON("div"), + children: [{ block: fakeJSON("div"), children: [] }], }); }); test("Chaining .add() creates a single branch in the tree", (t) => { // Selectors `div`, `.foo`, `#bar`, matching, e.g., `
` // and inserted in increasing specificity. - const node = RuleTree.Node.of(fakeItem("div"), [], None); - node.add(fakeItem(".foo")).add(fakeItem("#bar")); + const node = RuleTree.Node.of(fakeBlock("div"), [], None); + node.add(fakeBlock(".foo")).add(fakeBlock("#bar")); t.deepEqual(node.toJSON(), { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON("#bar"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON("#bar"), children: [] }], }, ], }); @@ -82,15 +89,15 @@ test("Chaining .add() creates a single branch in the tree", (t) => { */ test(".add() creates a single branch in the rule tree", (t) => { const tree = RuleTree.empty(); - tree.add([fakeItem("div"), fakeItem(".foo"), fakeItem("#bar")]); + tree.add([fakeBlock("div"), fakeBlock(".foo"), fakeBlock("#bar")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON("#bar"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON("#bar"), children: [] }], }, ], }, @@ -101,15 +108,15 @@ test(".add() does not change the order of items", (t) => { const tree = RuleTree.empty(); // Items are not correctly ordered (#bar has higher specificity). Rule tree // doesn't care. - tree.add([fakeItem("#bar"), fakeItem("div"), fakeItem(".foo")]); + tree.add([fakeBlock("#bar"), fakeBlock("div"), fakeBlock(".foo")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("#bar"), + block: fakeJSON("#bar"), children: [ { - item: fakeJSON("div"), - children: [{ item: fakeJSON(".foo"), children: [] }], + block: fakeJSON("div"), + children: [{ block: fakeJSON(".foo"), children: [] }], }, ], }, @@ -119,12 +126,12 @@ test(".add() does not change the order of items", (t) => { test(".add() duplicate identical but distinct selectors", (t) => { const tree = RuleTree.empty(); // Presumably two rules with selector `div` at different place in the sheet. - tree.add([fakeItem("div"), fakeItem("div")]); + tree.add([fakeBlock("div"), fakeBlock("div")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), - children: [{ item: fakeJSON("div"), children: [] }], + block: fakeJSON("div"), + children: [{ block: fakeJSON("div"), children: [] }], }, ]); }); @@ -132,27 +139,27 @@ test(".add() duplicate identical but distinct selectors", (t) => { test(".add() creates separate trees for entries that don't share initial selectors", (t) => { const tree = RuleTree.empty(); // Matching `
- tree.add([fakeItem("div"), fakeItem(".foo"), fakeItem("#bar")]); + tree.add([fakeBlock("div"), fakeBlock(".foo"), fakeBlock("#bar")]); // Matching `` // Since the first selector differ, we cannot share any part of the tree - tree.add([fakeItem("span"), fakeItem(".foo"), fakeItem("#bar")]); + tree.add([fakeBlock("span"), fakeBlock(".foo"), fakeBlock("#bar")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON("#bar"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON("#bar"), children: [] }], }, ], }, { - item: fakeJSON("span"), + block: fakeJSON("span"), children: [ { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON("#bar"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON("#bar"), children: [] }], }, ], }, @@ -160,21 +167,21 @@ test(".add() creates separate trees for entries that don't share initial selecto }); test(".add() share branches as long as selectors are the same", (t) => { - const div = fakeItem("div"); - const foo = fakeItem(".foo"); + const div = fakeBlock("div"); + const foo = fakeBlock(".foo"); const tree = RuleTree.empty(); - tree.add([div, foo, fakeItem("#bar")]); - tree.add([div, foo, fakeItem(".baz")]); + tree.add([div, foo, fakeBlock("#bar")]); + tree.add([div, foo, fakeBlock(".baz")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), + block: fakeJSON(".foo"), children: [ - { item: fakeJSON("#bar"), children: [] }, - { item: fakeJSON(".baz"), children: [] }, + { block: fakeJSON("#bar"), children: [] }, + { block: fakeJSON(".baz"), children: [] }, ], }, ], @@ -183,28 +190,28 @@ test(".add() share branches as long as selectors are the same", (t) => { }); test(".add() adds descendants when selectors are merely identical", (t) => { - const div = fakeItem("div"); + const div = fakeBlock("div"); const tree = RuleTree.empty(); - tree.add([div, fakeItem(".foo"), fakeItem("#bar")]); + tree.add([div, fakeBlock(".foo"), fakeBlock("#bar")]); // This is not an actual possible case. This corresponds to two `.foo` // selectors but each matches different elements, which is impossible. // Hence, the adding of the .foo / .baz branch under the initial .foo // looks very wrong but is actually the correct thing to do. In an actual // case, both .add would contain both .foo selector, since this rules with // identical selectors match the same elements. - tree.add([div, fakeItem(".foo"), fakeItem(".baz")]); + tree.add([div, fakeBlock(".foo"), fakeBlock(".baz")]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), + block: fakeJSON(".foo"), children: [ - { item: fakeJSON("#bar"), children: [] }, + { block: fakeJSON("#bar"), children: [] }, { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON(".baz"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON(".baz"), children: [] }], }, ], }, @@ -214,26 +221,26 @@ test(".add() adds descendants when selectors are merely identical", (t) => { }); test(".add() branches as soon as selectors differ", (t) => { - const div = fakeItem("div"); - const foo = fakeItem(".foo"); + const div = fakeBlock("div"); + const foo = fakeBlock(".foo"); const tree = RuleTree.empty(); - tree.add([div, foo, fakeItem("#bar")]); + tree.add([div, foo, fakeBlock("#bar")]); // Even if the selector is the same, the tree doesn't try to merge the branches. - tree.add([div, fakeItem(".baz"), foo]); + tree.add([div, fakeBlock(".baz"), foo]); t.deepEqual(tree.toJSON(), [ { - item: fakeJSON("div"), + block: fakeJSON("div"), children: [ { - item: fakeJSON(".foo"), - children: [{ item: fakeJSON("#bar"), children: [] }], + block: fakeJSON(".foo"), + children: [{ block: fakeJSON("#bar"), children: [] }], }, { - item: fakeJSON(".baz"), + block: fakeJSON(".baz"), children: [ { - item: fakeJSON(".foo"), + block: fakeJSON(".foo"), children: [], }, ], diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index ee26c11cb3..7403674811 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -7,6 +7,7 @@ }, "files": [ "src/ancestor-filter.ts", + "src/block.ts", "src/cascade.ts", "src/index.ts", "src/precedence/index.ts", diff --git a/packages/alfa-rules/src/sia-r83/rule.ts b/packages/alfa-rules/src/sia-r83/rule.ts index 479f88eeb6..21a48d7f51 100644 --- a/packages/alfa-rules/src/sia-r83/rule.ts +++ b/packages/alfa-rules/src/sia-r83/rule.ts @@ -504,7 +504,7 @@ function getUsedMediaRules( // for each of these rules, get all ancestor media rules in the CSS tree. return ancestorsInRuleTree( Cascade.of(root, device).get(element, context), - ).flatMap((node) => ancestorMediaRules(node.rule)); + ).flatMap((node) => ancestorMediaRules(node.block.rule)); } function usesMediaRule( diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index e06e72b8c4..ebac1742c2 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -332,7 +332,7 @@ export namespace Style { for (const node of cascade .get(element, context) .inclusiveAncestors()) { - declarations.push(...[...node.declarations].reverse()); + declarations.push(...[...node.block.declarations].reverse()); } } From 89090a197179912a2602f498c38600174c3208c2 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 18 Dec 2023 14:41:39 +0100 Subject: [PATCH 08/26] Add block builder with coupling --- packages/alfa-cascade/src/block.ts | 59 +++++++++++++++++++++-- packages/alfa-cascade/src/rule-tree.ts | 6 +-- packages/alfa-cascade/src/selector-map.ts | 1 - 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/alfa-cascade/src/block.ts b/packages/alfa-cascade/src/block.ts index edabebdf95..1ab7f3f9a1 100644 --- a/packages/alfa-cascade/src/block.ts +++ b/packages/alfa-cascade/src/block.ts @@ -1,14 +1,18 @@ import { Array } from "@siteimprove/alfa-array"; import { type Comparer, Comparison } from "@siteimprove/alfa-comparable"; -import { Declaration, Rule, StyleRule } from "@siteimprove/alfa-dom"; +import { Lexer } from "@siteimprove/alfa-css"; +import { Declaration, h, Rule, StyleRule } from "@siteimprove/alfa-dom"; import type { Equatable } from "@siteimprove/alfa-equatable"; import { Iterable } from "@siteimprove/alfa-iterable"; import type { Serializable } from "@siteimprove/alfa-json"; -import { Selector } from "@siteimprove/alfa-selector"; +import { None } from "@siteimprove/alfa-option"; +import type { Result } from "@siteimprove/alfa-result"; +import { Selector, Specificity, Universal } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; -import { Precedence } from "./precedence"; +import { Origin, Precedence } from "./precedence"; +import { UserAgent } from "./user-agent"; /** * While resolving cascade, a Block is a style rule that has been expanded with @@ -41,6 +45,23 @@ export class Block implements Equatable, Serializable { return new Block(rule, selector, Array.from(declarations), precedence); } + private static _empty = new Block( + h.rule.style("*", []), + Universal.of(None), + [], + { + origin: Origin.UserAgent, + specificity: Specificity.empty(), + order: Infinity, + }, + ); + /** + * @internal + */ + public static empty(): Block { + return this._empty; + } + private readonly _rule: StyleRule; private readonly _selector: Selector; private readonly _declarations: Array; @@ -110,6 +131,38 @@ export namespace Block { precedence: Precedence.JSON; } + /** + * Build Blocks from a style rule. + * + * @remarks + * Order is relative to the list of all style rules and thus cannot be inferred + * from the rule itself. + * + * A single rule creates more than one block. Rules with a list selector are + * split into their components. E.g., a `div, span { color: red }` rule will + * create one block for `div { color: red }`, and a similar one for `span`. + * Since all these blocks are declared at the same time, and are declaring + * the exact same declarations, they can safely share order. + */ + export function from( + rule: StyleRule, + order: number, + ): Result, string> { + return Selector.parse(Lexer.lex(rule.selector)).map(([_, selectors]) => { + const origin = rule.owner.includes(UserAgent) + ? Origin.UserAgent + : Origin.Author; + + return [...selectors].map((selector) => + Block.of(rule, selector, rule.style, { + origin, + order, + specificity: selector.specificity, + }), + ); + }); + } + export const compare: Comparer = (a, b) => Precedence.compare(a.precedence, b.precedence); } diff --git a/packages/alfa-cascade/src/rule-tree.ts b/packages/alfa-cascade/src/rule-tree.ts index c4be763daf..d4bf6668e1 100644 --- a/packages/alfa-cascade/src/rule-tree.ts +++ b/packages/alfa-cascade/src/rule-tree.ts @@ -82,11 +82,7 @@ export class RuleTree implements Serializable { // Rooting the forest at a fake node with no declaration. private readonly _root: RuleTree.Node = RuleTree.Node.of( - Block.of(h.rule.style("*", []), Universal.of(None), [], { - origin: Origin.UserAgent, - specificity: Specificity.empty(), - order: Infinity, - }), + Block.empty(), [], None, ); diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 9b32c14157..fcfc853f82 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -227,7 +227,6 @@ export namespace SelectorMap { origin, order, specificity: selector.specificity, - // specificity: selector.specificity, }); } } From e8206446acb8382adbae450cc989417b6f85eef1 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 18 Dec 2023 14:55:07 +0100 Subject: [PATCH 09/26] =?UTF-8?q?Use=20blo=C4=89k=20builder=20in=20selecto?= =?UTF-8?q?r=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/alfa-cascade/src/block.ts | 33 +++++++++++--------- packages/alfa-cascade/src/selector-map.ts | 38 ++++++----------------- 2 files changed, 28 insertions(+), 43 deletions(-) diff --git a/packages/alfa-cascade/src/block.ts b/packages/alfa-cascade/src/block.ts index 1ab7f3f9a1..f301e88685 100644 --- a/packages/alfa-cascade/src/block.ts +++ b/packages/alfa-cascade/src/block.ts @@ -132,7 +132,8 @@ export namespace Block { } /** - * Build Blocks from a style rule. + * Build Blocks from a style rule. Returns the last order used, that is unchanged + * if selector couldn't be parsed, increased by 1 otherwise. * * @remarks * Order is relative to the list of all style rules and thus cannot be inferred @@ -144,23 +145,27 @@ export namespace Block { * Since all these blocks are declared at the same time, and are declaring * the exact same declarations, they can safely share order. */ - export function from( - rule: StyleRule, - order: number, - ): Result, string> { - return Selector.parse(Lexer.lex(rule.selector)).map(([_, selectors]) => { + export function from(rule: StyleRule, order: number): [Array, number] { + let blocks = []; + + for (const [_, selectors] of Selector.parse(Lexer.lex(rule.selector))) { const origin = rule.owner.includes(UserAgent) ? Origin.UserAgent : Origin.Author; - return [...selectors].map((selector) => - Block.of(rule, selector, rule.style, { - origin, - order, - specificity: selector.specificity, - }), - ); - }); + order++; + + for (const selector of selectors) { + blocks.push( + Block.of(rule, selector, rule.style, { + origin, + order, + specificity: selector.specificity, + }), + ); + } + } + return [blocks, order]; } export const compare: Comparer = (a, b) => diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index fcfc853f82..ab5c5d6ce4 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -4,6 +4,7 @@ import { Device } from "@siteimprove/alfa-device"; import { Declaration, Element, + h, ImportRule, MediaRule, Rule, @@ -160,6 +161,8 @@ export class SelectorMap implements Serializable { * @internal */ export namespace SelectorMap { + import block = h.block; + export interface JSON { [key: string]: json.JSON; ids: Bucket.JSON; @@ -181,22 +184,8 @@ export namespace SelectorMap { const types = Bucket.empty(); const other: Array = []; - const add = ( - rule: StyleRule, - selector: Selector, - declarations: Iterable, - { origin, order }: Precedence, - ): void => { - const block = Block.of(rule, selector, Array.from(declarations), { - origin, - order, - specificity: - StyleRule.isStyleRule(rule) && rule.hint - ? Specificity.empty() - : selector.specificity, - }); - - const keySelector = selector.key; + const add = (block: Block): void => { + const keySelector = block.selector.key; if (!keySelector.isSome()) { other.push(block); @@ -215,20 +204,11 @@ export namespace SelectorMap { return; } - for (const [, selector] of Selector.parse(Lexer.lex(rule.selector))) { - const origin = rule.owner.includes(UserAgent) - ? Origin.UserAgent - : Origin.Author; - - order++; + let blocks: Array = []; + [blocks, order] = Block.from(rule, order); - for (const part of selector) { - add(rule, part, rule.style, { - origin, - order, - specificity: selector.specificity, - }); - } + for (const block of blocks) { + add(block); } } From cd9e1d68ba483d943fc9329dfb92e93fe4751eed Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 18 Dec 2023 15:22:51 +0100 Subject: [PATCH 10/26] Tighten typing --- .changeset/selfish-elephants-float.md | 7 ++++ packages/alfa-cascade/src/ancestor-filter.ts | 6 ++- packages/alfa-cascade/src/block.ts | 42 +++++++++++++------- packages/alfa-cascade/src/rule-tree.ts | 3 -- packages/alfa-cascade/src/selector-map.ts | 24 ++++------- 5 files changed, 47 insertions(+), 35 deletions(-) create mode 100644 .changeset/selfish-elephants-float.md diff --git a/.changeset/selfish-elephants-float.md b/.changeset/selfish-elephants-float.md new file mode 100644 index 0000000000..193f9bc2c0 --- /dev/null +++ b/.changeset/selfish-elephants-float.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Removed:** `AncestorFilter#match` has been made internal. + +se `!ANcestorFilter.canReject` instead, which is having less assumptions. diff --git a/packages/alfa-cascade/src/ancestor-filter.ts b/packages/alfa-cascade/src/ancestor-filter.ts index 3765dbf4cf..b157a711c6 100644 --- a/packages/alfa-cascade/src/ancestor-filter.ts +++ b/packages/alfa-cascade/src/ancestor-filter.ts @@ -8,6 +8,7 @@ import { Compound, Id, Selector, + type Simple, Type, } from "@siteimprove/alfa-selector"; @@ -100,7 +101,10 @@ export class AncestorFilter implements Serializable { } } - public matches(selector: Selector): boolean { + /** + * @internal + */ + public matches(selector: Simple): boolean { if (Id.isId(selector)) { return this._ids.has(selector.name); } diff --git a/packages/alfa-cascade/src/block.ts b/packages/alfa-cascade/src/block.ts index f301e88685..6f62482a33 100644 --- a/packages/alfa-cascade/src/block.ts +++ b/packages/alfa-cascade/src/block.ts @@ -4,10 +4,16 @@ import { Lexer } from "@siteimprove/alfa-css"; import { Declaration, h, Rule, StyleRule } from "@siteimprove/alfa-dom"; import type { Equatable } from "@siteimprove/alfa-equatable"; import { Iterable } from "@siteimprove/alfa-iterable"; -import type { Serializable } from "@siteimprove/alfa-json"; +import { Serializable } from "@siteimprove/alfa-json"; import { None } from "@siteimprove/alfa-option"; -import type { Result } from "@siteimprove/alfa-result"; -import { Selector, Specificity, Universal } from "@siteimprove/alfa-selector"; +import { + type Complex, + type Compound, + Selector, + type Simple, + Specificity, + Universal, +} from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; @@ -29,19 +35,25 @@ import { UserAgent } from "./user-agent"; * * @internal */ -export class Block implements Equatable, Serializable { +export class Block< + S extends Compound | Complex | Simple = Compound | Complex | Simple, + > + implements Equatable, Serializable> +{ /** * Create a block. * * @remarks * This does not validate coupling of the data. Prefer using Block.from() */ - public static of( + public static of< + S extends Compound | Complex | Simple = Compound | Complex | Simple, + >( rule: StyleRule, - selector: Selector, + selector: S, declarations: Iterable, precedence: Precedence, - ): Block { + ): Block { return new Block(rule, selector, Array.from(declarations), precedence); } @@ -63,13 +75,13 @@ export class Block implements Equatable, Serializable { } private readonly _rule: StyleRule; - private readonly _selector: Selector; + private readonly _selector: S; private readonly _declarations: Array; private readonly _precedence: Precedence; constructor( rule: StyleRule, - selector: Selector, + selector: S, declarations: Array, precedence: Precedence, ) { @@ -83,7 +95,7 @@ export class Block implements Equatable, Serializable { return this._rule; } - public get selector(): Selector { + public get selector(): S { return this._selector; } @@ -110,10 +122,10 @@ export class Block implements Equatable, Serializable { ); } - public toJSON(): Block.JSON { + public toJSON(): Block.JSON { return { rule: this._rule.toJSON(), - selector: this._selector.toJSON(), + selector: Serializable.toJSON(this._selector), declarations: Array.toJSON(this._declarations), precedence: Precedence.toJSON(this._precedence), }; @@ -123,10 +135,12 @@ export class Block implements Equatable, Serializable { * @internal */ export namespace Block { - export interface JSON { + export interface JSON< + S extends Compound | Complex | Simple = Compound | Complex | Simple, + > { [key: string]: json.JSON; rule: Rule.JSON; - selector: Selector.JSON; + selector: Serializable.ToJSON; declarations: Array; precedence: Precedence.JSON; } diff --git a/packages/alfa-cascade/src/rule-tree.ts b/packages/alfa-cascade/src/rule-tree.ts index d4bf6668e1..13a9f9bfa6 100644 --- a/packages/alfa-cascade/src/rule-tree.ts +++ b/packages/alfa-cascade/src/rule-tree.ts @@ -1,12 +1,9 @@ -import { Declaration, h, Rule } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; import * as json from "@siteimprove/alfa-json"; import { Serializable } from "@siteimprove/alfa-json"; import { None, Option } from "@siteimprove/alfa-option"; -import { Selector, Specificity, Universal } from "@siteimprove/alfa-selector"; import { Block } from "./block"; -import { Origin } from "./precedence"; /** * The rule tree is a data structure used for storing the rules that match each diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index ab5c5d6ce4..3e54437926 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -2,9 +2,7 @@ import { Array } from "@siteimprove/alfa-array"; import { Lexer } from "@siteimprove/alfa-css"; import { Device } from "@siteimprove/alfa-device"; import { - Declaration, Element, - h, ImportRule, MediaRule, Rule, @@ -17,20 +15,13 @@ import { Media } from "@siteimprove/alfa-media"; import { Option } from "@siteimprove/alfa-option"; import { Predicate } from "@siteimprove/alfa-predicate"; import { Refinement } from "@siteimprove/alfa-refinement"; -import { - Combinator, - Complex, - Context, - Selector, - Specificity, -} from "@siteimprove/alfa-selector"; +import { Combinator, Complex, Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; import { Block } from "./block"; -import { type Order, Origin, type Precedence } from "./precedence"; -import { UserAgent } from "./user-agent"; +import { type Order } from "./precedence"; const { equals, property } = Predicate; const { and } = Refinement; @@ -115,19 +106,20 @@ export class SelectorMap implements Serializable { const nodes: Array = []; const collect = (candidates: Iterable) => { - for (const node of candidates) { + for (const block of candidates) { if ( + // If the ancestor filter can reject the selector, escape filter.none((filter) => Iterable.every( - node.selector, + block.selector, and(isDescendantSelector, (selector) => filter.canReject(selector.left), ), ), ) && - node.selector.matches(element, context) + block.selector.matches(element, context) ) { - nodes.push(node); + nodes.push(block); } } }; @@ -161,8 +153,6 @@ export class SelectorMap implements Serializable { * @internal */ export namespace SelectorMap { - import block = h.block; - export interface JSON { [key: string]: json.JSON; ids: Bucket.JSON; From 37181d9cb071684279c6105b3847d9c4b7989cb4 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 18 Dec 2023 16:26:29 +0100 Subject: [PATCH 11/26] Add some tests --- packages/alfa-cascade/src/selector-map.ts | 116 +++--------------- .../test/ancestor-filter.spec.tsx | 3 +- packages/alfa-cascade/test/rule-tree.spec.ts | 22 +++- .../alfa-cascade/test/selector-map.spec.ts | 100 +++++++++++++++ packages/alfa-cascade/tsconfig.json | 3 +- 5 files changed, 137 insertions(+), 107 deletions(-) create mode 100644 packages/alfa-cascade/test/selector-map.spec.ts diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 3e54437926..6490e5b256 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -44,7 +44,7 @@ const isDescendantSelector = and( * Rules are indexed according to their key selector, which is the selector * that a given element MUST match in order for the rest of the selector to also * match. A key selector can be either an ID selector, a class selector, or a - * type selector. In a relative selector, the key selector will be the + * type selector. In a complex selector, the key selector will be the * right-most selector, e.g. given `main .foo + div` the key selector would be * `div`. In a compound selector, the key selector will be left-most selector, * e.g. given `div.foo` the key selector would also be `div`. @@ -53,7 +53,7 @@ const isDescendantSelector = and( * matching `main .foo + div` must be a `div`. Reciprocally, a `
` * can only match selectors whose key selector is `div` or `bar`. Thus, filtering * on key selectors decrease the search space for matching selector before the - * computation heavy steps of traversing the DOM to loo for siblings or ancestors. + * computation heavy steps of traversing the DOM to look for siblings or ancestors. * * @privateRemarks * Internally, the selector map has three maps and a list in one of which it @@ -98,6 +98,10 @@ export class SelectorMap implements Serializable { this._other = other; } + /** + * Get all blocks matching a given element and context, an optional + * ancestor filter can be provided to optimize performances. + */ public get( element: Element, context: Context, @@ -109,14 +113,12 @@ export class SelectorMap implements Serializable { for (const block of candidates) { if ( // If the ancestor filter can reject the selector, escape - filter.none((filter) => - Iterable.every( - block.selector, - and(isDescendantSelector, (selector) => - filter.canReject(selector.left), - ), - ), + filter.none( + (filter) => + isDescendantSelector(block.selector) && + filter.canReject(block.selector.left), ) && + // otherwise, do the actual match. block.selector.matches(element, context) ) { nodes.push(block); @@ -256,96 +258,9 @@ export namespace SelectorMap { return SelectorMap.of(ids, classes, types, other); } - // export class Node implements Serializable { - // public static of( - // rule: Rule, - // selector: Selector, - // declarations: Iterable, - // origin: Origin, - // order: Order, - // ): Node { - // return new Node(rule, selector, declarations, origin, order); - // } - // - // private readonly _rule: Rule; - // private readonly _selector: Selector; - // private readonly _declarations: Iterable; - // private readonly _origin: Origin; - // private readonly _order: Order; - // private readonly _specificity: Specificity; - // - // private constructor( - // rule: Rule, - // selector: Selector, - // declarations: Iterable, - // origin: Origin, - // order: Order, - // ) { - // this._rule = rule; - // this._selector = selector; - // this._declarations = declarations; - // this._origin = origin; - // this._order = order; - // - // // For style rules that are presentational hints, the specificity will - // // always be 0 regardless of the selector. - // // Otherwise, use the specificity of the selector. - // this._specificity = - // StyleRule.isStyleRule(rule) && rule.hint - // ? Specificity.empty() - // : selector.specificity; - // } - // - // public get rule(): Rule { - // return this._rule; - // } - // - // public get selector(): Selector { - // return this._selector; - // } - // - // public get declarations(): Iterable { - // return this._declarations; - // } - // - // public get origin(): Origin { - // return this._origin; - // } - // - // public get order(): Order { - // return this._order; - // } - // - // public get specificity(): Specificity { - // return this._specificity; - // } - // - // public toJSON(): Node.JSON { - // return { - // rule: this._rule.toJSON(), - // selector: this._selector.toJSON(), - // declarations: [...this._declarations].map((declaration) => - // declaration.toJSON(), - // ), - // origin: this._origin, - // order: this._order, - // specificity: this._specificity.toJSON(), - // }; - // } - // } - // - // export namespace Node { - // export interface JSON { - // [key: string]: json.JSON; - // rule: Rule.JSON; - // selector: Selector.JSON; - // declarations: Array; - // origin: Origin.JSON; - // order: Order.JSON; - // specificity: Specificity.JSON; - // } - // } - + /** + * @internal + */ export class Bucket implements Serializable { public static empty(): Bucket { return new Bucket(new Map()); @@ -385,6 +300,9 @@ export namespace SelectorMap { } } + /** + * @internal + */ export namespace Bucket { export type JSON = Array<[string, Array]>; } diff --git a/packages/alfa-cascade/test/ancestor-filter.spec.tsx b/packages/alfa-cascade/test/ancestor-filter.spec.tsx index be1c86e3d1..e2ffe7f659 100644 --- a/packages/alfa-cascade/test/ancestor-filter.spec.tsx +++ b/packages/alfa-cascade/test/ancestor-filter.spec.tsx @@ -1,3 +1,4 @@ +import type { Simple } from "@siteimprove/alfa-selector"; import { Assertions, test } from "@siteimprove/alfa-test"; import { parse } from "@siteimprove/alfa-selector/test/parser"; @@ -95,7 +96,7 @@ function match( "hashFooSel", "hashBarSel", ] as const) { - t.equal(filter.matches(selectors[sel]), matching.includes(sel)); + t.equal(filter.matches(selectors[sel] as Simple), matching.includes(sel)); } } diff --git a/packages/alfa-cascade/test/rule-tree.spec.ts b/packages/alfa-cascade/test/rule-tree.spec.ts index 10030358ea..af201f2cb8 100644 --- a/packages/alfa-cascade/test/rule-tree.spec.ts +++ b/packages/alfa-cascade/test/rule-tree.spec.ts @@ -1,6 +1,11 @@ import { h } from "@siteimprove/alfa-dom"; import { None } from "@siteimprove/alfa-option"; -import { Specificity } from "@siteimprove/alfa-selector"; +import { + Complex, + Compound, + type Simple, + Specificity, +} from "@siteimprove/alfa-selector"; import { parse } from "@siteimprove/alfa-selector/test/parser"; import { test } from "@siteimprove/alfa-test"; @@ -10,11 +15,16 @@ import { Block } from "../src/block"; import { Origin } from "../src/precedence"; function fakeBlock(selector: string): Block { - return Block.of(h.rule.style(selector, []), parse(selector), [], { - origin: Origin.UserAgent, - specificity: Specificity.empty(), - order: -1, - }); + return Block.of( + h.rule.style(selector, []), + parse(selector) as Compound | Complex | Simple, + [], + { + origin: Origin.UserAgent, + specificity: Specificity.empty(), + order: -1, + }, + ); } function fakeJSON(selector: string): Block.JSON { diff --git a/packages/alfa-cascade/test/selector-map.spec.ts b/packages/alfa-cascade/test/selector-map.spec.ts new file mode 100644 index 0000000000..1d8c1650cd --- /dev/null +++ b/packages/alfa-cascade/test/selector-map.spec.ts @@ -0,0 +1,100 @@ +import { Array } from "@siteimprove/alfa-array"; +import { Device } from "@siteimprove/alfa-device"; +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; +import { SelectorMap } from "../src/selector-map"; + +import { Block } from "../src/block"; + +const device = Device.standard(); + +test(".from() builds a selector map with a single rule", (t) => { + const actual = SelectorMap.from( + [h.sheet([h.rule.style("div", { foo: "not parsed" })])], + device, + ); + + t.deepEqual(actual.toJSON(), { + ids: [], + classes: [], + types: [ + [ + "div", + [ + { + declarations: [ + { important: false, name: "foo", value: "not parsed" }, + ], + precedence: { + order: 1, + origin: 2, + specificity: { a: 0, b: 0, c: 1 }, + }, + rule: { + selector: "div", + style: [{ important: false, name: "foo", value: "not parsed" }], + type: "style", + }, + selector: { + key: "div", + name: "div", + namespace: null, + specificity: { a: 0, b: 0, c: 1 }, + type: "type", + }, + }, + ], + ], + ], + other: [], + }); +}); + +test(".from() rejects rules w%ith invalid selectors", (t) => { + const actual = SelectorMap.from( + [h.sheet([h.rule.style(":non-existent", { foo: "not parsed" })])], + device, + ); + + t.deepEqual(actual.toJSON(), { ids: [], classes: [], types: [], other: [] }); +}); + +test(".from() stores rules in increasing order, amongst all sheets", (t) => { + const rules = [ + h.rule.style("foo", { foo: "bar" }), + h.rule.style("bar", { foo: "bar" }), + h.rule.style(".bar", { foo: "bar" }), + h.rule.style(".foo", { foo: "bar" }), + h.rule.style("#hello", { foo: "bar" }), + h.rule.style("::focus", { foo: "bar" }), + ]; + + const actual = SelectorMap.from( + [ + h.sheet([rules[0], rules[1]]), + h.sheet([rules[2]]), + h.sheet([rules[3], rules[4]]), + h.sheet([rules[5]]), + ], + device, + ); + + const blocks = rules + // Each block is computed with an order equal to the index of the rule in the array. + // This is what we want because rules are inserted in order in the sheets. + .map(Block.from) + .map(([blocks, _]) => Array.toJSON(blocks)); + + t.deepEqual(actual.toJSON(), { + ids: [["hello", blocks[4]]], + classes: [ + ["bar", blocks[2]], + ["foo", blocks[3]], + ], + types: [ + ["foo", blocks[0]], + ["bar", blocks[1]], + ], + other: blocks[5], + }); +}); diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 7403674811..85dd19f883 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -18,7 +18,8 @@ "src/selector-map.ts", "src/user-agent.ts", "test/ancestor-filter.spec.tsx", - "test/rule-tree.spec.ts" + "test/rule-tree.spec.ts", + "test/selector-map.spec.ts" ], "references": [ { "path": "../alfa-cache" }, From a2c1725e4c40a8aefd6c106a646aeb6b320d849f Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 19 Dec 2023 10:24:41 +0100 Subject: [PATCH 12/26] Store rule type in abstract class --- packages/alfa-dom/src/style/rule.ts | 21 ++++++++++++++----- packages/alfa-dom/src/style/rule/condition.ts | 18 +++++++++++----- packages/alfa-dom/src/style/rule/font-face.ts | 12 ++++------- packages/alfa-dom/src/style/rule/grouping.ts | 16 +++++++++----- packages/alfa-dom/src/style/rule/import.ts | 11 ++++------ packages/alfa-dom/src/style/rule/keyframe.ts | 9 ++++---- packages/alfa-dom/src/style/rule/keyframes.ts | 10 ++++----- packages/alfa-dom/src/style/rule/media.ts | 14 ++++--------- packages/alfa-dom/src/style/rule/namespace.ts | 9 ++++---- packages/alfa-dom/src/style/rule/page.ts | 9 ++++---- packages/alfa-dom/src/style/rule/style.ts | 9 ++++---- packages/alfa-dom/src/style/rule/supports.ts | 14 ++++--------- 12 files changed, 76 insertions(+), 76 deletions(-) diff --git a/packages/alfa-dom/src/style/rule.ts b/packages/alfa-dom/src/style/rule.ts index 108a6e0b66..df768d0967 100644 --- a/packages/alfa-dom/src/style/rule.ts +++ b/packages/alfa-dom/src/style/rule.ts @@ -22,11 +22,20 @@ import { /** * @public */ -export abstract class Rule implements Equatable, Serializable { +export abstract class Rule + implements Equatable, Serializable +{ protected _owner: Option = None; protected _parent: Option = None; + private readonly _type: T; - protected constructor() {} + protected constructor(type: T) { + this._type = type; + } + + public get type(): T { + return this._type; + } public get owner(): Option { return this._owner; @@ -61,7 +70,9 @@ export abstract class Rule implements Equatable, Serializable { return value === this; } - public abstract toJSON(): Rule.JSON; + public toJSON(): Rule.JSON { + return { type: this._type }; + } /** * @internal @@ -94,9 +105,9 @@ export abstract class Rule implements Equatable, Serializable { * @public */ export namespace Rule { - export interface JSON { + export interface JSON { [key: string]: json.JSON; - type: string; + type: T; } export function from(json: StyleRule.JSON): StyleRule; diff --git a/packages/alfa-dom/src/style/rule/condition.ts b/packages/alfa-dom/src/style/rule/condition.ts index 4a8b0b9cc4..0e363a3a2b 100644 --- a/packages/alfa-dom/src/style/rule/condition.ts +++ b/packages/alfa-dom/src/style/rule/condition.ts @@ -4,11 +4,13 @@ import { GroupingRule } from "./grouping"; /** * @public */ -export abstract class ConditionRule extends GroupingRule { +export abstract class ConditionRule< + T extends string = string, +> extends GroupingRule { protected readonly _condition: string; - protected constructor(condition: string, rules: Array) { - super(rules); + protected constructor(type: T, condition: string, rules: Array) { + super(type, rules); this._condition = condition; } @@ -17,14 +19,20 @@ export abstract class ConditionRule extends GroupingRule { return this._condition; } - public abstract toJSON(): ConditionRule.JSON; + public toJSON(): ConditionRule.JSON { + return { + ...super.toJSON(), + condition: this._condition, + }; + } } /** * @public */ export namespace ConditionRule { - export interface JSON extends GroupingRule.JSON { + export interface JSON + extends GroupingRule.JSON { condition: string; } diff --git a/packages/alfa-dom/src/style/rule/font-face.ts b/packages/alfa-dom/src/style/rule/font-face.ts index be1733e567..0c6bf1deb2 100644 --- a/packages/alfa-dom/src/style/rule/font-face.ts +++ b/packages/alfa-dom/src/style/rule/font-face.ts @@ -1,7 +1,5 @@ import { Trampoline } from "@siteimprove/alfa-trampoline"; -import * as json from "@siteimprove/alfa-json"; - import { Block } from "../block"; import { Declaration } from "../declaration"; import { Rule } from "../rule"; @@ -9,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class FontFaceRule extends Rule { +export class FontFaceRule extends Rule<"font-face"> { public static of(declarations: Iterable): FontFaceRule { return new FontFaceRule(Array.from(declarations)); } @@ -17,7 +15,7 @@ export class FontFaceRule extends Rule { private readonly _style: Block; private constructor(declarations: Array) { - super(); + super("font-face"); this._style = Block.of( declarations.filter((declaration) => declaration._attachParent(this)), @@ -30,7 +28,7 @@ export class FontFaceRule extends Rule { public toJSON(): FontFaceRule.JSON { return { - type: "font-face", + ...super.toJSON(), style: this._style.toJSON(), }; } @@ -46,9 +44,7 @@ export class FontFaceRule extends Rule { * @public */ export namespace FontFaceRule { - export interface JSON { - [key: string]: json.JSON; - type: "font-face"; + export interface JSON extends Rule.JSON<"font-face"> { style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/grouping.ts b/packages/alfa-dom/src/style/rule/grouping.ts index 796f1e9c25..6a1ad62b47 100644 --- a/packages/alfa-dom/src/style/rule/grouping.ts +++ b/packages/alfa-dom/src/style/rule/grouping.ts @@ -1,13 +1,14 @@ +import { Array } from "@siteimprove/alfa-array"; import { Rule } from "../rule"; /** * @public */ -export abstract class GroupingRule extends Rule { +export abstract class GroupingRule extends Rule { protected readonly _rules: Array; - protected constructor(rules: Array) { - super(); + protected constructor(type: T, rules: Array) { + super(type); this._rules = rules.filter((rule) => rule._attachParent(this)); } @@ -20,14 +21,19 @@ export abstract class GroupingRule extends Rule { yield* this._rules; } - public abstract toJSON(): GroupingRule.JSON; + public toJSON(): GroupingRule.JSON { + return { + ...super.toJSON(), + rules: Array.toJSON(this._rules), + }; + } } /** * @public */ export namespace GroupingRule { - export interface JSON extends Rule.JSON { + export interface JSON extends Rule.JSON { rules: Array; } diff --git a/packages/alfa-dom/src/style/rule/import.ts b/packages/alfa-dom/src/style/rule/import.ts index f76d231cf7..0e913c411d 100644 --- a/packages/alfa-dom/src/style/rule/import.ts +++ b/packages/alfa-dom/src/style/rule/import.ts @@ -10,7 +10,7 @@ import { ConditionRule } from "./condition"; /** * @public */ -export class ImportRule extends ConditionRule { +export class ImportRule extends ConditionRule<"import"> { public static of( href: string, sheet: Sheet, @@ -24,7 +24,7 @@ export class ImportRule extends ConditionRule { private readonly _queries: Media.List; private constructor(href: string, sheet: Sheet, condition: Option) { - super(condition.getOr("all"), []); + super("import", condition.getOr("all"), []); this._href = href; this._sheet = sheet; @@ -51,9 +51,7 @@ export class ImportRule extends ConditionRule { public toJSON(): ImportRule.JSON { return { - type: "import", - rules: [...this._sheet.rules].map((rule) => rule.toJSON()), - condition: this._condition, + ...super.toJSON(), href: this._href, }; } @@ -67,8 +65,7 @@ export class ImportRule extends ConditionRule { * @public */ export namespace ImportRule { - export interface JSON extends ConditionRule.JSON { - type: "import"; + export interface JSON extends ConditionRule.JSON<"import"> { href: string; } diff --git a/packages/alfa-dom/src/style/rule/keyframe.ts b/packages/alfa-dom/src/style/rule/keyframe.ts index 28710519b0..64ce815ebf 100644 --- a/packages/alfa-dom/src/style/rule/keyframe.ts +++ b/packages/alfa-dom/src/style/rule/keyframe.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class KeyframeRule extends Rule { +export class KeyframeRule extends Rule<"keyframe"> { public static of( key: string, declarations: Iterable, @@ -19,7 +19,7 @@ export class KeyframeRule extends Rule { private readonly _style: Block; private constructor(key: string, declarations: Array) { - super(); + super("keyframe"); this._key = key; this._style = Block.of( @@ -37,7 +37,7 @@ export class KeyframeRule extends Rule { public toJSON(): KeyframeRule.JSON { return { - type: "keyframe", + ...super.toJSON(), key: this._key, style: this._style.toJSON(), }; @@ -56,8 +56,7 @@ export class KeyframeRule extends Rule { * @public */ export namespace KeyframeRule { - export interface JSON extends Rule.JSON { - type: "keyframe"; + export interface JSON extends Rule.JSON<"keyframe"> { key: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/keyframes.ts b/packages/alfa-dom/src/style/rule/keyframes.ts index b123f8a04f..ff889cf530 100644 --- a/packages/alfa-dom/src/style/rule/keyframes.ts +++ b/packages/alfa-dom/src/style/rule/keyframes.ts @@ -6,7 +6,7 @@ import { GroupingRule } from "./grouping"; /** * @public */ -export class KeyframesRule extends GroupingRule { +export class KeyframesRule extends GroupingRule<"keyframes"> { public static of(name: string, rules: Iterable): KeyframesRule { return new KeyframesRule(name, Array.from(rules)); } @@ -14,7 +14,7 @@ export class KeyframesRule extends GroupingRule { private readonly _name: string; private constructor(name: string, rules: Array) { - super(rules); + super("keyframes", rules); this._name = name; } @@ -25,8 +25,7 @@ export class KeyframesRule extends GroupingRule { public toJSON(): KeyframesRule.JSON { return { - type: "keyframes", - rules: [...this.rules].map((rule) => rule.toJSON()), + ...super.toJSON(), name: this._name, }; } @@ -44,8 +43,7 @@ export class KeyframesRule extends GroupingRule { * @public */ export namespace KeyframesRule { - export interface JSON extends GroupingRule.JSON { - type: "keyframes"; + export interface JSON extends GroupingRule.JSON<"keyframes"> { name: string; } diff --git a/packages/alfa-dom/src/style/rule/media.ts b/packages/alfa-dom/src/style/rule/media.ts index 7f60842ee4..2c9c560023 100644 --- a/packages/alfa-dom/src/style/rule/media.ts +++ b/packages/alfa-dom/src/style/rule/media.ts @@ -11,7 +11,7 @@ const { map, join } = Iterable; /** * @public */ -export class MediaRule extends ConditionRule { +export class MediaRule extends ConditionRule<"media"> { public static of(condition: string, rules: Iterable): MediaRule { return new MediaRule(condition, Array.from(rules)); } @@ -19,7 +19,7 @@ export class MediaRule extends ConditionRule { private readonly _queries: Media.List; private constructor(condition: string, rules: Array) { - super(condition, rules); + super("media", condition, rules); this._queries = Media.parse(Lexer.lex(condition)) .map(([, queries]) => queries) @@ -31,11 +31,7 @@ export class MediaRule extends ConditionRule { } public toJSON(): MediaRule.JSON { - return { - type: "media", - rules: [...this._rules].map((rule) => rule.toJSON()), - condition: this._condition, - }; + return super.toJSON(); } public toString(): string { @@ -52,9 +48,7 @@ export class MediaRule extends ConditionRule { * @public */ export namespace MediaRule { - export interface JSON extends ConditionRule.JSON { - type: "media"; - } + export interface JSON extends ConditionRule.JSON<"media"> {} export function isMediaRule(value: unknown): value is MediaRule { return value instanceof MediaRule; diff --git a/packages/alfa-dom/src/style/rule/namespace.ts b/packages/alfa-dom/src/style/rule/namespace.ts index d43e26ca0f..eeaf129d57 100644 --- a/packages/alfa-dom/src/style/rule/namespace.ts +++ b/packages/alfa-dom/src/style/rule/namespace.ts @@ -6,7 +6,7 @@ import { Rule } from "../rule"; /** * @public */ -export class NamespaceRule extends Rule { +export class NamespaceRule extends Rule<"namespace"> { public static of(namespace: string, prefix: Option): NamespaceRule { return new NamespaceRule(namespace, prefix); } @@ -15,7 +15,7 @@ export class NamespaceRule extends Rule { private readonly _prefix: Option; private constructor(namespace: string, prefix: Option) { - super(); + super("namespace"); this._namespace = namespace; this._prefix = prefix; @@ -31,7 +31,7 @@ export class NamespaceRule extends Rule { public toJSON(): NamespaceRule.JSON { return { - type: "namespace", + ...super.toJSON(), namespace: this._namespace, prefix: this._prefix.getOr(null), }; @@ -48,8 +48,7 @@ export class NamespaceRule extends Rule { * @public */ export namespace NamespaceRule { - export interface JSON extends Rule.JSON { - type: "namespace"; + export interface JSON extends Rule.JSON<"namespace"> { namespace: string; prefix: string | null; } diff --git a/packages/alfa-dom/src/style/rule/page.ts b/packages/alfa-dom/src/style/rule/page.ts index e1f20b4ba1..94bc8f29f7 100644 --- a/packages/alfa-dom/src/style/rule/page.ts +++ b/packages/alfa-dom/src/style/rule/page.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class PageRule extends Rule { +export class PageRule extends Rule<"page"> { public static of( selector: string, declarations: Iterable, @@ -19,7 +19,7 @@ export class PageRule extends Rule { private readonly _style: Block; private constructor(selector: string, declarations: Array) { - super(); + super("page"); this._selector = selector; this._style = Block.of( @@ -37,7 +37,7 @@ export class PageRule extends Rule { public toJSON(): PageRule.JSON { return { - type: "page", + ...super.toJSON(), selector: this._selector, style: this._style.toJSON(), }; @@ -56,8 +56,7 @@ export class PageRule extends Rule { * @public */ export namespace PageRule { - export interface JSON extends Rule.JSON { - type: "page"; + export interface JSON extends Rule.JSON<"page"> { selector: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/style.ts b/packages/alfa-dom/src/style/rule/style.ts index fd6615f17c..0d9bf0ace8 100644 --- a/packages/alfa-dom/src/style/rule/style.ts +++ b/packages/alfa-dom/src/style/rule/style.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class StyleRule extends Rule { +export class StyleRule extends Rule<"style"> { public static of( selector: string, declarations: Iterable, @@ -25,7 +25,7 @@ export class StyleRule extends Rule { declarations: Array, hint: boolean, ) { - super(); + super("style"); this._selector = selector; this._style = Block.of( @@ -48,7 +48,7 @@ export class StyleRule extends Rule { public toJSON(): StyleRule.JSON { return { - type: "style", + ...super.toJSON(), selector: this._selector, style: this._style.toJSON(), }; @@ -65,8 +65,7 @@ export class StyleRule extends Rule { * @public */ export namespace StyleRule { - export interface JSON extends Rule.JSON { - type: "style"; + export interface JSON extends Rule.JSON<"style"> { selector: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/supports.ts b/packages/alfa-dom/src/style/rule/supports.ts index f33f7d1ed3..59a5b7e9d3 100644 --- a/packages/alfa-dom/src/style/rule/supports.ts +++ b/packages/alfa-dom/src/style/rule/supports.ts @@ -6,21 +6,17 @@ import { ConditionRule } from "./condition"; /** * @public */ -export class SupportsRule extends ConditionRule { +export class SupportsRule extends ConditionRule<"supports"> { public static of(condition: string, rules: Iterable): SupportsRule { return new SupportsRule(condition, Array.from(rules)); } private constructor(condition: string, rules: Array) { - super(condition, rules); + super("supports", condition, rules); } public toJSON(): SupportsRule.JSON { - return { - type: "supports", - rules: [...this._rules].map((rule) => rule.toJSON()), - condition: this._condition, - }; + return super.toJSON(); } public toString(): string { @@ -38,9 +34,7 @@ export class SupportsRule extends ConditionRule { * @public */ export namespace SupportsRule { - export interface JSON extends ConditionRule.JSON { - type: "supports"; - } + export interface JSON extends ConditionRule.JSON<"supports"> {} export function isSupportsRue(value: unknown): value is SupportsRule { return value instanceof SupportsRule; From 9d93ba47889e186008d808e0a47d845686d45282 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 19 Dec 2023 10:27:00 +0100 Subject: [PATCH 13/26] Revert "Store rule type in abstract class" This reverts commit a2c1725e4c40a8aefd6c106a646aeb6b320d849f. --- packages/alfa-dom/src/style/rule.ts | 21 +++++-------------- packages/alfa-dom/src/style/rule/condition.ts | 18 +++++----------- packages/alfa-dom/src/style/rule/font-face.ts | 12 +++++++---- packages/alfa-dom/src/style/rule/grouping.ts | 16 +++++--------- packages/alfa-dom/src/style/rule/import.ts | 11 ++++++---- packages/alfa-dom/src/style/rule/keyframe.ts | 9 ++++---- packages/alfa-dom/src/style/rule/keyframes.ts | 10 +++++---- packages/alfa-dom/src/style/rule/media.ts | 14 +++++++++---- packages/alfa-dom/src/style/rule/namespace.ts | 9 ++++---- packages/alfa-dom/src/style/rule/page.ts | 9 ++++---- packages/alfa-dom/src/style/rule/style.ts | 9 ++++---- packages/alfa-dom/src/style/rule/supports.ts | 14 +++++++++---- 12 files changed, 76 insertions(+), 76 deletions(-) diff --git a/packages/alfa-dom/src/style/rule.ts b/packages/alfa-dom/src/style/rule.ts index df768d0967..108a6e0b66 100644 --- a/packages/alfa-dom/src/style/rule.ts +++ b/packages/alfa-dom/src/style/rule.ts @@ -22,20 +22,11 @@ import { /** * @public */ -export abstract class Rule - implements Equatable, Serializable -{ +export abstract class Rule implements Equatable, Serializable { protected _owner: Option = None; protected _parent: Option = None; - private readonly _type: T; - protected constructor(type: T) { - this._type = type; - } - - public get type(): T { - return this._type; - } + protected constructor() {} public get owner(): Option { return this._owner; @@ -70,9 +61,7 @@ export abstract class Rule return value === this; } - public toJSON(): Rule.JSON { - return { type: this._type }; - } + public abstract toJSON(): Rule.JSON; /** * @internal @@ -105,9 +94,9 @@ export abstract class Rule * @public */ export namespace Rule { - export interface JSON { + export interface JSON { [key: string]: json.JSON; - type: T; + type: string; } export function from(json: StyleRule.JSON): StyleRule; diff --git a/packages/alfa-dom/src/style/rule/condition.ts b/packages/alfa-dom/src/style/rule/condition.ts index 0e363a3a2b..4a8b0b9cc4 100644 --- a/packages/alfa-dom/src/style/rule/condition.ts +++ b/packages/alfa-dom/src/style/rule/condition.ts @@ -4,13 +4,11 @@ import { GroupingRule } from "./grouping"; /** * @public */ -export abstract class ConditionRule< - T extends string = string, -> extends GroupingRule { +export abstract class ConditionRule extends GroupingRule { protected readonly _condition: string; - protected constructor(type: T, condition: string, rules: Array) { - super(type, rules); + protected constructor(condition: string, rules: Array) { + super(rules); this._condition = condition; } @@ -19,20 +17,14 @@ export abstract class ConditionRule< return this._condition; } - public toJSON(): ConditionRule.JSON { - return { - ...super.toJSON(), - condition: this._condition, - }; - } + public abstract toJSON(): ConditionRule.JSON; } /** * @public */ export namespace ConditionRule { - export interface JSON - extends GroupingRule.JSON { + export interface JSON extends GroupingRule.JSON { condition: string; } diff --git a/packages/alfa-dom/src/style/rule/font-face.ts b/packages/alfa-dom/src/style/rule/font-face.ts index 0c6bf1deb2..be1733e567 100644 --- a/packages/alfa-dom/src/style/rule/font-face.ts +++ b/packages/alfa-dom/src/style/rule/font-face.ts @@ -1,5 +1,7 @@ import { Trampoline } from "@siteimprove/alfa-trampoline"; +import * as json from "@siteimprove/alfa-json"; + import { Block } from "../block"; import { Declaration } from "../declaration"; import { Rule } from "../rule"; @@ -7,7 +9,7 @@ import { Rule } from "../rule"; /** * @public */ -export class FontFaceRule extends Rule<"font-face"> { +export class FontFaceRule extends Rule { public static of(declarations: Iterable): FontFaceRule { return new FontFaceRule(Array.from(declarations)); } @@ -15,7 +17,7 @@ export class FontFaceRule extends Rule<"font-face"> { private readonly _style: Block; private constructor(declarations: Array) { - super("font-face"); + super(); this._style = Block.of( declarations.filter((declaration) => declaration._attachParent(this)), @@ -28,7 +30,7 @@ export class FontFaceRule extends Rule<"font-face"> { public toJSON(): FontFaceRule.JSON { return { - ...super.toJSON(), + type: "font-face", style: this._style.toJSON(), }; } @@ -44,7 +46,9 @@ export class FontFaceRule extends Rule<"font-face"> { * @public */ export namespace FontFaceRule { - export interface JSON extends Rule.JSON<"font-face"> { + export interface JSON { + [key: string]: json.JSON; + type: "font-face"; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/grouping.ts b/packages/alfa-dom/src/style/rule/grouping.ts index 6a1ad62b47..796f1e9c25 100644 --- a/packages/alfa-dom/src/style/rule/grouping.ts +++ b/packages/alfa-dom/src/style/rule/grouping.ts @@ -1,14 +1,13 @@ -import { Array } from "@siteimprove/alfa-array"; import { Rule } from "../rule"; /** * @public */ -export abstract class GroupingRule extends Rule { +export abstract class GroupingRule extends Rule { protected readonly _rules: Array; - protected constructor(type: T, rules: Array) { - super(type); + protected constructor(rules: Array) { + super(); this._rules = rules.filter((rule) => rule._attachParent(this)); } @@ -21,19 +20,14 @@ export abstract class GroupingRule extends Rule { yield* this._rules; } - public toJSON(): GroupingRule.JSON { - return { - ...super.toJSON(), - rules: Array.toJSON(this._rules), - }; - } + public abstract toJSON(): GroupingRule.JSON; } /** * @public */ export namespace GroupingRule { - export interface JSON extends Rule.JSON { + export interface JSON extends Rule.JSON { rules: Array; } diff --git a/packages/alfa-dom/src/style/rule/import.ts b/packages/alfa-dom/src/style/rule/import.ts index 0e913c411d..f76d231cf7 100644 --- a/packages/alfa-dom/src/style/rule/import.ts +++ b/packages/alfa-dom/src/style/rule/import.ts @@ -10,7 +10,7 @@ import { ConditionRule } from "./condition"; /** * @public */ -export class ImportRule extends ConditionRule<"import"> { +export class ImportRule extends ConditionRule { public static of( href: string, sheet: Sheet, @@ -24,7 +24,7 @@ export class ImportRule extends ConditionRule<"import"> { private readonly _queries: Media.List; private constructor(href: string, sheet: Sheet, condition: Option) { - super("import", condition.getOr("all"), []); + super(condition.getOr("all"), []); this._href = href; this._sheet = sheet; @@ -51,7 +51,9 @@ export class ImportRule extends ConditionRule<"import"> { public toJSON(): ImportRule.JSON { return { - ...super.toJSON(), + type: "import", + rules: [...this._sheet.rules].map((rule) => rule.toJSON()), + condition: this._condition, href: this._href, }; } @@ -65,7 +67,8 @@ export class ImportRule extends ConditionRule<"import"> { * @public */ export namespace ImportRule { - export interface JSON extends ConditionRule.JSON<"import"> { + export interface JSON extends ConditionRule.JSON { + type: "import"; href: string; } diff --git a/packages/alfa-dom/src/style/rule/keyframe.ts b/packages/alfa-dom/src/style/rule/keyframe.ts index 64ce815ebf..28710519b0 100644 --- a/packages/alfa-dom/src/style/rule/keyframe.ts +++ b/packages/alfa-dom/src/style/rule/keyframe.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class KeyframeRule extends Rule<"keyframe"> { +export class KeyframeRule extends Rule { public static of( key: string, declarations: Iterable, @@ -19,7 +19,7 @@ export class KeyframeRule extends Rule<"keyframe"> { private readonly _style: Block; private constructor(key: string, declarations: Array) { - super("keyframe"); + super(); this._key = key; this._style = Block.of( @@ -37,7 +37,7 @@ export class KeyframeRule extends Rule<"keyframe"> { public toJSON(): KeyframeRule.JSON { return { - ...super.toJSON(), + type: "keyframe", key: this._key, style: this._style.toJSON(), }; @@ -56,7 +56,8 @@ export class KeyframeRule extends Rule<"keyframe"> { * @public */ export namespace KeyframeRule { - export interface JSON extends Rule.JSON<"keyframe"> { + export interface JSON extends Rule.JSON { + type: "keyframe"; key: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/keyframes.ts b/packages/alfa-dom/src/style/rule/keyframes.ts index ff889cf530..b123f8a04f 100644 --- a/packages/alfa-dom/src/style/rule/keyframes.ts +++ b/packages/alfa-dom/src/style/rule/keyframes.ts @@ -6,7 +6,7 @@ import { GroupingRule } from "./grouping"; /** * @public */ -export class KeyframesRule extends GroupingRule<"keyframes"> { +export class KeyframesRule extends GroupingRule { public static of(name: string, rules: Iterable): KeyframesRule { return new KeyframesRule(name, Array.from(rules)); } @@ -14,7 +14,7 @@ export class KeyframesRule extends GroupingRule<"keyframes"> { private readonly _name: string; private constructor(name: string, rules: Array) { - super("keyframes", rules); + super(rules); this._name = name; } @@ -25,7 +25,8 @@ export class KeyframesRule extends GroupingRule<"keyframes"> { public toJSON(): KeyframesRule.JSON { return { - ...super.toJSON(), + type: "keyframes", + rules: [...this.rules].map((rule) => rule.toJSON()), name: this._name, }; } @@ -43,7 +44,8 @@ export class KeyframesRule extends GroupingRule<"keyframes"> { * @public */ export namespace KeyframesRule { - export interface JSON extends GroupingRule.JSON<"keyframes"> { + export interface JSON extends GroupingRule.JSON { + type: "keyframes"; name: string; } diff --git a/packages/alfa-dom/src/style/rule/media.ts b/packages/alfa-dom/src/style/rule/media.ts index 2c9c560023..7f60842ee4 100644 --- a/packages/alfa-dom/src/style/rule/media.ts +++ b/packages/alfa-dom/src/style/rule/media.ts @@ -11,7 +11,7 @@ const { map, join } = Iterable; /** * @public */ -export class MediaRule extends ConditionRule<"media"> { +export class MediaRule extends ConditionRule { public static of(condition: string, rules: Iterable): MediaRule { return new MediaRule(condition, Array.from(rules)); } @@ -19,7 +19,7 @@ export class MediaRule extends ConditionRule<"media"> { private readonly _queries: Media.List; private constructor(condition: string, rules: Array) { - super("media", condition, rules); + super(condition, rules); this._queries = Media.parse(Lexer.lex(condition)) .map(([, queries]) => queries) @@ -31,7 +31,11 @@ export class MediaRule extends ConditionRule<"media"> { } public toJSON(): MediaRule.JSON { - return super.toJSON(); + return { + type: "media", + rules: [...this._rules].map((rule) => rule.toJSON()), + condition: this._condition, + }; } public toString(): string { @@ -48,7 +52,9 @@ export class MediaRule extends ConditionRule<"media"> { * @public */ export namespace MediaRule { - export interface JSON extends ConditionRule.JSON<"media"> {} + export interface JSON extends ConditionRule.JSON { + type: "media"; + } export function isMediaRule(value: unknown): value is MediaRule { return value instanceof MediaRule; diff --git a/packages/alfa-dom/src/style/rule/namespace.ts b/packages/alfa-dom/src/style/rule/namespace.ts index eeaf129d57..d43e26ca0f 100644 --- a/packages/alfa-dom/src/style/rule/namespace.ts +++ b/packages/alfa-dom/src/style/rule/namespace.ts @@ -6,7 +6,7 @@ import { Rule } from "../rule"; /** * @public */ -export class NamespaceRule extends Rule<"namespace"> { +export class NamespaceRule extends Rule { public static of(namespace: string, prefix: Option): NamespaceRule { return new NamespaceRule(namespace, prefix); } @@ -15,7 +15,7 @@ export class NamespaceRule extends Rule<"namespace"> { private readonly _prefix: Option; private constructor(namespace: string, prefix: Option) { - super("namespace"); + super(); this._namespace = namespace; this._prefix = prefix; @@ -31,7 +31,7 @@ export class NamespaceRule extends Rule<"namespace"> { public toJSON(): NamespaceRule.JSON { return { - ...super.toJSON(), + type: "namespace", namespace: this._namespace, prefix: this._prefix.getOr(null), }; @@ -48,7 +48,8 @@ export class NamespaceRule extends Rule<"namespace"> { * @public */ export namespace NamespaceRule { - export interface JSON extends Rule.JSON<"namespace"> { + export interface JSON extends Rule.JSON { + type: "namespace"; namespace: string; prefix: string | null; } diff --git a/packages/alfa-dom/src/style/rule/page.ts b/packages/alfa-dom/src/style/rule/page.ts index 94bc8f29f7..e1f20b4ba1 100644 --- a/packages/alfa-dom/src/style/rule/page.ts +++ b/packages/alfa-dom/src/style/rule/page.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class PageRule extends Rule<"page"> { +export class PageRule extends Rule { public static of( selector: string, declarations: Iterable, @@ -19,7 +19,7 @@ export class PageRule extends Rule<"page"> { private readonly _style: Block; private constructor(selector: string, declarations: Array) { - super("page"); + super(); this._selector = selector; this._style = Block.of( @@ -37,7 +37,7 @@ export class PageRule extends Rule<"page"> { public toJSON(): PageRule.JSON { return { - ...super.toJSON(), + type: "page", selector: this._selector, style: this._style.toJSON(), }; @@ -56,7 +56,8 @@ export class PageRule extends Rule<"page"> { * @public */ export namespace PageRule { - export interface JSON extends Rule.JSON<"page"> { + export interface JSON extends Rule.JSON { + type: "page"; selector: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/style.ts b/packages/alfa-dom/src/style/rule/style.ts index 0d9bf0ace8..fd6615f17c 100644 --- a/packages/alfa-dom/src/style/rule/style.ts +++ b/packages/alfa-dom/src/style/rule/style.ts @@ -7,7 +7,7 @@ import { Rule } from "../rule"; /** * @public */ -export class StyleRule extends Rule<"style"> { +export class StyleRule extends Rule { public static of( selector: string, declarations: Iterable, @@ -25,7 +25,7 @@ export class StyleRule extends Rule<"style"> { declarations: Array, hint: boolean, ) { - super("style"); + super(); this._selector = selector; this._style = Block.of( @@ -48,7 +48,7 @@ export class StyleRule extends Rule<"style"> { public toJSON(): StyleRule.JSON { return { - ...super.toJSON(), + type: "style", selector: this._selector, style: this._style.toJSON(), }; @@ -65,7 +65,8 @@ export class StyleRule extends Rule<"style"> { * @public */ export namespace StyleRule { - export interface JSON extends Rule.JSON<"style"> { + export interface JSON extends Rule.JSON { + type: "style"; selector: string; style: Block.JSON; } diff --git a/packages/alfa-dom/src/style/rule/supports.ts b/packages/alfa-dom/src/style/rule/supports.ts index 59a5b7e9d3..f33f7d1ed3 100644 --- a/packages/alfa-dom/src/style/rule/supports.ts +++ b/packages/alfa-dom/src/style/rule/supports.ts @@ -6,17 +6,21 @@ import { ConditionRule } from "./condition"; /** * @public */ -export class SupportsRule extends ConditionRule<"supports"> { +export class SupportsRule extends ConditionRule { public static of(condition: string, rules: Iterable): SupportsRule { return new SupportsRule(condition, Array.from(rules)); } private constructor(condition: string, rules: Array) { - super("supports", condition, rules); + super(condition, rules); } public toJSON(): SupportsRule.JSON { - return super.toJSON(); + return { + type: "supports", + rules: [...this._rules].map((rule) => rule.toJSON()), + condition: this._condition, + }; } public toString(): string { @@ -34,7 +38,9 @@ export class SupportsRule extends ConditionRule<"supports"> { * @public */ export namespace SupportsRule { - export interface JSON extends ConditionRule.JSON<"supports"> {} + export interface JSON extends ConditionRule.JSON { + type: "supports"; + } export function isSupportsRue(value: unknown): value is SupportsRule { return value instanceof SupportsRule; From 1f0f3c9be6a133b31360ec99272034daa4c14efe Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 19 Dec 2023 11:28:07 +0100 Subject: [PATCH 14/26] Add selector map tests --- packages/alfa-cascade/src/cascade.ts | 4 +- packages/alfa-cascade/src/rule-tree.ts | 5 +- packages/alfa-cascade/src/selector-map.ts | 23 +- .../alfa-cascade/test/selector-map.spec.ts | 100 -------- .../alfa-cascade/test/selector-map.spec.tsx | 216 ++++++++++++++++++ packages/alfa-cascade/tsconfig.json | 2 +- packages/alfa-dom/src/h.ts | 8 + 7 files changed, 238 insertions(+), 120 deletions(-) delete mode 100644 packages/alfa-cascade/test/selector-map.spec.ts create mode 100644 packages/alfa-cascade/test/selector-map.spec.tsx diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 765cc977d4..c07e5e4c39 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -81,9 +81,7 @@ export class Cascade implements Serializable { return this._entries .get(element, Cache.empty) .get(context, () => - this._rules.add( - this._selectors.get(element, context, filter).sort(Block.compare), - ), + this._rules.add(this._selectors.get(element, context, filter)), ); } diff --git a/packages/alfa-cascade/src/rule-tree.ts b/packages/alfa-cascade/src/rule-tree.ts index 13a9f9bfa6..6917a70552 100644 --- a/packages/alfa-cascade/src/rule-tree.ts +++ b/packages/alfa-cascade/src/rule-tree.ts @@ -93,8 +93,7 @@ export class RuleTree implements Serializable { * @remarks * The rules are assumed to be: * 1. all matching the same element; and - * 2. ordered in increasing cascade sort order (lower precedence rule first); and - * 3. be all the rules matching that element. + * 2. be all the rules matching that element. * * It is up to the caller to ensure this is true, as the tree itself cannot * check that (notably, it has no access to the DOM tree to ensure the rules @@ -109,7 +108,7 @@ export class RuleTree implements Serializable { public add(rules: Iterable): RuleTree.Node { let parent = this._root; - for (const block of rules) { + for (const block of Iterable.sortWith(rules, Block.compare)) { // Insert the next rule into the current parent, using the returned rule // entry as the parent of the next rule to insert. This way, we gradually // build up a path of rule entries and then return the final entry to the diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 6490e5b256..7972093a4b 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -102,14 +102,12 @@ export class SelectorMap implements Serializable { * Get all blocks matching a given element and context, an optional * ancestor filter can be provided to optimize performances. */ - public get( + public *get( element: Element, context: Context, filter: Option, - ): Array { - const nodes: Array = []; - - const collect = (candidates: Iterable) => { + ): Iterable { + function* collect(candidates: Iterable): Iterable { for (const block of candidates) { if ( // If the ancestor filter can reject the selector, escape @@ -121,24 +119,22 @@ export class SelectorMap implements Serializable { // otherwise, do the actual match. block.selector.matches(element, context) ) { - nodes.push(block); + yield block; } } - }; + } for (const id of element.id) { - collect(this._ids.get(id)); + yield* collect(this._ids.get(id)); } - collect(this._types.get(element.name)); + yield* collect(this._types.get(element.name)); for (const className of element.classes) { - collect(this._classes.get(className)); + yield* collect(this._classes.get(className)); } - collect(this._other); - - return nodes; + yield* collect(this._other); } public toJSON(): SelectorMap.JSON { @@ -189,6 +185,7 @@ export namespace SelectorMap { }; const visit = (rule: Rule) => { + // For style rule, we just store its blocks. if (StyleRule.isStyleRule(rule)) { // Style rules with empty style blocks aren't relevant and so can be // skipped entirely. diff --git a/packages/alfa-cascade/test/selector-map.spec.ts b/packages/alfa-cascade/test/selector-map.spec.ts deleted file mode 100644 index 1d8c1650cd..0000000000 --- a/packages/alfa-cascade/test/selector-map.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Array } from "@siteimprove/alfa-array"; -import { Device } from "@siteimprove/alfa-device"; -import { h } from "@siteimprove/alfa-dom"; -import { test } from "@siteimprove/alfa-test"; -import { SelectorMap } from "../src/selector-map"; - -import { Block } from "../src/block"; - -const device = Device.standard(); - -test(".from() builds a selector map with a single rule", (t) => { - const actual = SelectorMap.from( - [h.sheet([h.rule.style("div", { foo: "not parsed" })])], - device, - ); - - t.deepEqual(actual.toJSON(), { - ids: [], - classes: [], - types: [ - [ - "div", - [ - { - declarations: [ - { important: false, name: "foo", value: "not parsed" }, - ], - precedence: { - order: 1, - origin: 2, - specificity: { a: 0, b: 0, c: 1 }, - }, - rule: { - selector: "div", - style: [{ important: false, name: "foo", value: "not parsed" }], - type: "style", - }, - selector: { - key: "div", - name: "div", - namespace: null, - specificity: { a: 0, b: 0, c: 1 }, - type: "type", - }, - }, - ], - ], - ], - other: [], - }); -}); - -test(".from() rejects rules w%ith invalid selectors", (t) => { - const actual = SelectorMap.from( - [h.sheet([h.rule.style(":non-existent", { foo: "not parsed" })])], - device, - ); - - t.deepEqual(actual.toJSON(), { ids: [], classes: [], types: [], other: [] }); -}); - -test(".from() stores rules in increasing order, amongst all sheets", (t) => { - const rules = [ - h.rule.style("foo", { foo: "bar" }), - h.rule.style("bar", { foo: "bar" }), - h.rule.style(".bar", { foo: "bar" }), - h.rule.style(".foo", { foo: "bar" }), - h.rule.style("#hello", { foo: "bar" }), - h.rule.style("::focus", { foo: "bar" }), - ]; - - const actual = SelectorMap.from( - [ - h.sheet([rules[0], rules[1]]), - h.sheet([rules[2]]), - h.sheet([rules[3], rules[4]]), - h.sheet([rules[5]]), - ], - device, - ); - - const blocks = rules - // Each block is computed with an order equal to the index of the rule in the array. - // This is what we want because rules are inserted in order in the sheets. - .map(Block.from) - .map(([blocks, _]) => Array.toJSON(blocks)); - - t.deepEqual(actual.toJSON(), { - ids: [["hello", blocks[4]]], - classes: [ - ["bar", blocks[2]], - ["foo", blocks[3]], - ], - types: [ - ["foo", blocks[0]], - ["bar", blocks[1]], - ], - other: blocks[5], - }); -}); diff --git a/packages/alfa-cascade/test/selector-map.spec.tsx b/packages/alfa-cascade/test/selector-map.spec.tsx new file mode 100644 index 0000000000..f8b579ca7f --- /dev/null +++ b/packages/alfa-cascade/test/selector-map.spec.tsx @@ -0,0 +1,216 @@ +import { Array } from "@siteimprove/alfa-array"; +import { Device } from "@siteimprove/alfa-device"; +import { h, StyleRule } from "@siteimprove/alfa-dom"; +import { None, Option } from "@siteimprove/alfa-option"; +import { Context } from "@siteimprove/alfa-selector"; +import { test } from "@siteimprove/alfa-test"; +import { AncestorFilter } from "../src/ancestor-filter"; +import { SelectorMap } from "../src/selector-map"; + +import { Block } from "../src/block"; + +const device = Device.standard(); + +function ruleToBlockJSON(rule: StyleRule, order: number): Array { + return Array.toJSON(Block.from(rule, order)[0]); +} + +/** + * This initial test should have the full explicit JSON rather than rely on ruleToBlockJSON + * in order to circumvent possible issues in Block.from. + */ +test(".from() builds a selector map with a single rule", (t) => { + const actual = SelectorMap.from( + [h.sheet([h.rule.style("div", { foo: "not parsed" })])], + device, + ); + + t.deepEqual(actual.toJSON(), { + ids: [], + classes: [], + types: [ + [ + "div", + [ + { + declarations: [ + { important: false, name: "foo", value: "not parsed" }, + ], + precedence: { + order: 1, + origin: 2, + specificity: { a: 0, b: 0, c: 1 }, + }, + rule: { + selector: "div", + style: [{ important: false, name: "foo", value: "not parsed" }], + type: "style", + }, + selector: { + key: "div", + name: "div", + namespace: null, + specificity: { a: 0, b: 0, c: 1 }, + type: "type", + }, + }, + ], + ], + ], + other: [], + }); +}); + +test(".from() rejects rules with invalid selectors", (t) => { + const actual = SelectorMap.from( + [h.sheet([h.rule.style(":non-existent", { foo: "not parsed" })])], + device, + ); + + t.deepEqual(actual.toJSON(), { ids: [], classes: [], types: [], other: [] }); +}); + +test(".from() stores rules in increasing order, amongst all non-disabled sheets", (t) => { + const rules = [ + h.rule.style("foo", { foo: "bar" }), + h.rule.style("bar", { foo: "bar" }), + h.rule.style(".bar", { foo: "bar" }), + h.rule.style(".foo", { foo: "bar" }), + h.rule.style("#hello", { foo: "bar" }), + h.rule.style("::focus", { foo: "bar" }), + ]; + + const actual = SelectorMap.from( + [ + h.sheet([rules[0], rules[1]]), + h.sheet([rules[2]]), + h.sheet([h.rule.style("div", { foo: "bar" })], true), + h.sheet([rules[3], rules[4]]), + h.sheet([rules[5]]), + ], + device, + ); + + // Each block is computed with an order equal to the index of the rule in the array. + // This is what we want because rules are inserted in order in the sheets. + const blocks = rules.map(ruleToBlockJSON); + + t.deepEqual(actual.toJSON(), { + ids: [["hello", blocks[4]]], + classes: [ + ["bar", blocks[2]], + ["foo", blocks[3]], + ], + types: [ + ["foo", blocks[0]], + ["bar", blocks[1]], + ], + other: blocks[5], + }); +}); + +test(".from() only recurses into media rules that match the device", (t) => { + const rule = h.rule.style("foo", { foo: "bar" }); + const actual = SelectorMap.from( + [ + h.sheet([h.rule.media("screen", [rule])]), + h.sheet([h.rule.media("print", [h.rule.style("bar", { foo: "bar" })])]), + ], + device, + ); + + t.deepEqual(actual.toJSON(), { + ids: [], + classes: [], + types: [["foo", ruleToBlockJSON(rule, 0)]], + other: [], + }); +}); + +test(".from() only recurses into import rules that match the device", (t) => { + const rule = h.rule.style("foo", { foo: "bar" }); + const actual = SelectorMap.from( + [ + h.sheet([h.rule.importRule("foo.com", h.sheet([rule]), "screen")]), + h.sheet([ + h.rule.importRule( + "bar.com", + h.sheet([h.rule.style("bar", { foo: "bar" })]), + "print", + ), + ]), + ], + device, + ); + + t.deepEqual(actual.toJSON(), { + ids: [], + classes: [], + types: [["foo", ruleToBlockJSON(rule, 0)]], + other: [], + }); +}); + +test("#get() returns all blocks whose selector match an element", (t) => { + const rules = [ + h.rule.style("div", { foo: "bar" }), + h.rule.style("span", { foo: "bar" }), + h.rule.style(".bar", { foo: "bar" }), + h.rule.style(".foo", { foo: "bar" }), + h.rule.style("#hello", { foo: "bar" }), + h.rule.style("::focus", { foo: "bar" }), + ]; + const map = SelectorMap.from([h.sheet(rules)], device); + + const blocks = rules.map(ruleToBlockJSON); + + const element =
; + + t.deepEqual(Array.toJSON([...map.get(element, Context.empty(), None)]), [ + ...blocks[0], + ...blocks[3], + ]); +}); + +/** + * This test uses an incorrect ancestor filter to show that it takes precedence + * over the actual matching of the selector. + */ +test("#get() respects ancestor filter", (t) => { + const rules = [ + h.rule.style("span", { foo: "foo" }), + h.rule.style("div span", { bar: "bar" }), + ]; + const map = SelectorMap.from([h.sheet(rules)], device); + const blocks = rules.map(ruleToBlockJSON); + + const badFilter = AncestorFilter.empty(); + badFilter.add(
); + badFilter.add(); + + const goodFilter = AncestorFilter.empty(); + goodFilter.add(
); + goodFilter.add(
); + goodFilter.add(); + + const target = Hello; + const _ =
{target}
; + + // The filter is incorrect by not having the `
` and therefore rejects the `div span` rule. + t.deepEqual( + Array.toJSON([...map.get(target, Context.empty(), Option.of(badFilter))]), + blocks[0], + ); + + // The filter is correct and let the `div span` rule be matched. + t.deepEqual( + Array.toJSON([...map.get(target, Context.empty(), Option.of(goodFilter))]), + blocks[0].concat(blocks[1]), + ); + + // Without any filter, actual match is performed, and we get both rules. + t.deepEqual( + Array.toJSON([...map.get(target, Context.empty(), None)]), + blocks[0].concat(blocks[1]), + ); +}); diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 85dd19f883..98fd124c48 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -19,7 +19,7 @@ "src/user-agent.ts", "test/ancestor-filter.spec.tsx", "test/rule-tree.spec.ts", - "test/selector-map.spec.ts" + "test/selector-map.spec.tsx" ], "references": [ { "path": "../alfa-cache" }, diff --git a/packages/alfa-dom/src/h.ts b/packages/alfa-dom/src/h.ts index fdb6eedd70..ab19c8394c 100644 --- a/packages/alfa-dom/src/h.ts +++ b/packages/alfa-dom/src/h.ts @@ -11,6 +11,7 @@ import { Element, FontFaceRule, Fragment, + ImportRule, KeyframeRule, KeyframesRule, MediaRule, @@ -255,6 +256,13 @@ export namespace h { return FontFaceRule.of(block(declarations)); } + export function importRule( + url: string, + sheet: Sheet, + condition?: string, + ): ImportRule { + return ImportRule.of(url, sheet, Option.from(condition)); + } export function keyframe( key: string, declarations: Array | Record, From fc471cba7e11c928c1ece2f3a7c02f6b7683d066 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 19 Dec 2023 15:47:19 +0100 Subject: [PATCH 15/26] Add some documentation and improve custom getting --- packages/alfa-cascade/src/cascade.ts | 92 +++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index c07e5e4c39..4c785ec273 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -8,8 +8,6 @@ import { Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; import { AncestorFilter } from "./ancestor-filter"; -import { Block } from "./block"; -import { Precedence } from "./precedence"; import { RuleTree } from "./rule-tree"; import { SelectorMap } from "./selector-map"; import { UserAgent } from "./user-agent"; @@ -17,6 +15,22 @@ import { UserAgent } from "./user-agent"; /** * {@link https://drafts.csswg.org/css-cascade-5/} * + * @remarks + * The cascade associate to each element a node into a rule tree. + * A single rule tree is built for each document or shadow root. The cascade + * lazily fills it upon need and caches the associated node for each element. + * + * Upon creating a cascade, the full rule tree is built for the empty context + * in order to leverage the ancestor filter during tree traversal. This assumes + * that we will often query style of elements in an empty context (the default) + * and thus benefit from pre-building it for all elements. + * + * For specific contexts, we only add the nodes in the rule tree as needed. We assume + * that we mostly query only a few elements in a specific context, and that the cost + * of rebuilding a full cascade would be too expensive. + * + * The cascade automatically includes the user agent style sheet. + * * @public */ export class Cascade implements Serializable { @@ -57,7 +71,11 @@ export class Cascade implements Serializable { const visit = (node: Node): void => { if (Element.isElement(node)) { - this.get(node, context, Option.of(filter)); + // Since we are traversing the full DOM tree and maintaining our own + // ancestor filter on the way, use the simple #add. + + // Entering an element: add it to the rule tree, and to the ancestor filter. + this.add(node, context, Option.of(filter)); filter.add(node); } @@ -66,6 +84,7 @@ export class Cascade implements Serializable { } if (Element.isElement(node)) { + // Exiting an element: remove it from the ancestor filter. filter.remove(node); } }; @@ -73,16 +92,71 @@ export class Cascade implements Serializable { visit(root); } - public get( + /** + * Add an element to the rule tree, returns the associated node. + * + * @remarks + * This is idempotent since the rule tree already checks physical identity + * of selectors upon insertion. However, calling it too often is bad for performance. + */ + private add( element: Element, context: Context = Context.empty(), filter: Option = None, ): RuleTree.Node { - return this._entries - .get(element, Cache.empty) - .get(context, () => - this._rules.add(this._selectors.get(element, context, filter)), - ); + return this._rules.add(this._selectors.get(element, context, filter)); + } + + /** + * Adds an element to the tree, with a custom ancestor filter. + * + * @remarks + * A new ancestor filter is built and filled with the element's ancestors. + * When building the full cascade for a DOM tree, this is pointless and the + * faster #add should be used instead. When looking up the style of a single + * element, we assume shat the time spend going up the DOM tree to build an + * ancestor filter will be saved by matching less selectors. + */ + private addAncestors(element: Element, context: Context): RuleTree.Node { + const filter = AncestorFilter.empty(); + // Because CSS selectors do not cross shadow or document boundaries, + // only get ancestors in the same tree. + // Adding elements to the ancestor filter is commutative, so we + // can add them from bottom to root without reversing the sequence first. + element + .ancestors() + .filter(Element.isElement) + .forEach(filter.add.bind(filter)); + + return this._rules.add( + this._selectors.get(element, context, Option.of(filter)), + ); + } + + /** + * Get the rule tree node associated with an element. + * + * @remarks + * This also adds the element to the rule tree if needed. That is, the rule + * tree is build lazily upon need. For the empty context, we pre-build the full + * tree, so we can benefit from an ancestor filter as we traverse the full DOM tree. + * + * For other contexts, we assume that we will only need the style of a few elements + * (e.g., when a link is focused we normally only need the style of the link itself). + * Therefore, pre-building the full tree is not worth the cost nor the saving we'd + * get with an ancestor filter. + */ + public get( + element: Element, + context: Context = Context.empty(), + ): RuleTree.Node { + return this._entries.get(element, Cache.empty).get( + context, + // If the entry hasn't been cached already, we assume we are querying + // for a single element and pay the price of building its custom ancestor + // filter, hopefully saving on the matching cost. + () => this.addAncestors(element, context), + ); } public toJSON(): Cascade.JSON { From 04a36b2fa6b90c440996422a6f90856b2345686a Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 19 Dec 2023 16:34:08 +0100 Subject: [PATCH 16/26] Rename Cascade.of into Cascade.from --- .changeset/stale-crews-wonder.md | 7 + docs/review/api/alfa-cascade.api.md | 64 ++++----- packages/alfa-cascade/src/cascade.ts | 147 ++++++++++++++------- packages/alfa-cascade/test/cascade.spec.ts | 7 + packages/alfa-cascade/tsconfig.json | 1 + packages/alfa-rules/src/sia-r83/rule.ts | 2 +- packages/alfa-style/src/style.ts | 2 +- 7 files changed, 141 insertions(+), 89 deletions(-) create mode 100644 .changeset/stale-crews-wonder.md create mode 100644 packages/alfa-cascade/test/cascade.spec.ts diff --git a/.changeset/stale-crews-wonder.md b/.changeset/stale-crews-wonder.md new file mode 100644 index 0000000000..c94c4058a4 --- /dev/null +++ b/.changeset/stale-crews-wonder.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Breaking:** `Cascade.of` has been renamed `Cascade.from`. + +This matches the functinolatiy which is doing significant amount of work to prepare the cascade, and therefore aligns better with the rest of the naming scheme. diff --git a/docs/review/api/alfa-cascade.api.md b/docs/review/api/alfa-cascade.api.md index 407bd13c3d..b9e0017f3a 100644 --- a/docs/review/api/alfa-cascade.api.md +++ b/docs/review/api/alfa-cascade.api.md @@ -4,11 +4,17 @@ ```ts +import { Array as Array_2 } from '@siteimprove/alfa-array'; +import { Cache } from '@siteimprove/alfa-cache'; +import { Comparer } from '@siteimprove/alfa-comparable'; +import { Complex } from '@siteimprove/alfa-selector'; +import { Compound } from '@siteimprove/alfa-selector'; import { Context } from '@siteimprove/alfa-selector'; import { Declaration } from '@siteimprove/alfa-dom'; import { Device } from '@siteimprove/alfa-device'; import { Document } from '@siteimprove/alfa-dom'; import { Element } from '@siteimprove/alfa-dom'; +import type { Equatable } from '@siteimprove/alfa-equatable'; import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; import * as json from '@siteimprove/alfa-json'; import { Option } from '@siteimprove/alfa-option'; @@ -17,15 +23,18 @@ import { Selector } from '@siteimprove/alfa-selector'; import { Serializable } from '@siteimprove/alfa-json'; import { Shadow } from '@siteimprove/alfa-dom'; import { Sheet } from '@siteimprove/alfa-dom'; +import { Simple } from '@siteimprove/alfa-selector'; +import { Specificity } from '@siteimprove/alfa-selector/src/specificity'; +import { StyleRule } from '@siteimprove/alfa-dom'; // @public (undocumented) export class Cascade implements Serializable { - // Warning: (ae-forgotten-export) The symbol "AncestorFilter" needs to be exported by the entry point index.d.ts + static from(root: Document | Shadow, device: Device): Cascade; + get(element: Element, context?: Context): RuleTree.Node; + // Warning: (ae-forgotten-export) The symbol "SelectorMap" needs to be exported by the entry point index.d.ts // - // (undocumented) - get(element: Element, context?: Context, filter?: Option): RuleTree.Node; - // (undocumented) - static of(node: Document | Shadow, device: Device): Cascade; + // @internal + static of(root: Document | Shadow, device: Device, selectors: SelectorMap, rules: RuleTree, entries: Cache>): Cascade; // (undocumented) toJSON(): Cascade.JSON; } @@ -42,8 +51,6 @@ export namespace Cascade { root: Document.JSON | Shadow.JSON; // (undocumented) rules: RuleTree.JSON; - // Warning: (ae-forgotten-export) The symbol "SelectorMap" needs to be exported by the entry point index.d.ts - // // (undocumented) selectors: SelectorMap.JSON; } @@ -51,8 +58,10 @@ export namespace Cascade { // @public export class RuleTree implements Serializable { + // Warning: (ae-forgotten-export) The symbol "Block" needs to be exported by the entry point index.d.ts + // // @internal - add(rules: Iterable_2): RuleTree.Node; + add(rules: Iterable_2): RuleTree.Node; // (undocumented) static empty(): RuleTree; // (undocumented) @@ -61,52 +70,25 @@ export class RuleTree implements Serializable { // @public (undocumented) export namespace RuleTree { - // @internal - export interface Item { - // (undocumented) - declarations: Iterable_2; - // (undocumented) - rule: Rule; - // (undocumented) - selector: Selector; - } - // (undocumented) - export namespace Item { - // (undocumented) - export interface JSON { - // (undocumented) - [key: string]: json.JSON; - // (undocumented) - declarations: Array; - // (undocumented) - rule: Rule.JSON; - // (undocumented) - selector: Selector.JSON; - } - } // (undocumented) export type JSON = Array; // (undocumented) export class Node implements Serializable { // @internal - add(item: Item): Node; + add(block: Block): Node; // (undocumented) ancestors(): Iterable_2; // (undocumented) - get children(): Array; + get block(): Block; // (undocumented) - get declarations(): Iterable_2; + get children(): Array; // (undocumented) inclusiveAncestors(): Iterable_2; // (undocumented) - static of({ rule, selector, declarations }: Item, children: Array, parent: Option): Node; + static of(block: Block, children: Array, parent: Option): Node; // (undocumented) get parent(): Option; // (undocumented) - get rule(): Rule; - // (undocumented) - get selector(): Selector; - // (undocumented) toJSON(): Node.JSON; } // (undocumented) @@ -116,9 +98,9 @@ export namespace RuleTree { // (undocumented) [key: string]: json.JSON; // (undocumented) - children: Array; + block: Block.JSON; // (undocumented) - item: Item.JSON; + children: Array; } } } diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 4c785ec273..4d90319626 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -16,7 +16,7 @@ import { UserAgent } from "./user-agent"; * {@link https://drafts.csswg.org/css-cascade-5/} * * @remarks - * The cascade associate to each element a node into a rule tree. + * The cascade associates to each element a node into a rule tree. * A single rule tree is built for each document or shadow root. The cascade * lazily fills it upon need and caches the associated node for each element. * @@ -39,57 +39,106 @@ export class Cascade implements Serializable { Cache >(); - public static of(node: Document | Shadow, device: Device): Cascade { - return this._cascades - .get(node, Cache.empty) - .get(device, () => new Cascade(node, device)); + /** + * Unsafely create a cascade. + * + * @remarks + * This doesn't check coupling of data. This stores a cache that can still be + * accessed (and modified) from outside. This is only useful for writing tests + * without including the User Agent style sheet in the cascade. + * + * Do not use. Use Cascade.from() instead. Seriously. + * + * @internal + */ + public static of( + root: Document | Shadow, + device: Device, + selectors: SelectorMap, + rules: RuleTree, + entries: Cache>, + ): Cascade { + return new Cascade(root, device, selectors, rules, entries); } + /** + * Build a cascade. + * + * @privateRemarks + * This needs to be here rather than in the namespace because it also updates + * the global cascades cache. + */ + public static from(root: Document | Shadow, device: Device): Cascade { + // Caching all existing cascade since we need to maintain one for each + // document or shadow tree. + return this._cascades.get(root, Cache.empty).get(device, () => { + // Build a selector map with the User Agent style sheet, and the root style sheets. + const selectors = SelectorMap.from([UserAgent, ...root.style], device); + const rules = RuleTree.empty(); + const entries = Cache.empty>(); + + // Perform a baseline cascade with an empty context to benefit from ancestor + // filtering. As getting style information with an empty context will be the + // common case, we benefit a lot from pre-computing this style information + // with an ancestor filter applied. + const context = Context.empty(); + const filter = AncestorFilter.empty(); + + const visit = (node: Node): void => { + if (Element.isElement(node)) { + // Since we are traversing the full DOM tree and maintaining our own + // ancestor filter on the way, use the simple #add. + + // Entering an element: add it to the rule tree, and to the ancestor filter. + entries + .get(node, Cache.empty) + .get(context, () => + rules.add(selectors.get(node, context, Option.of(filter))), + ); + filter.add(node); + } + + for (const child of node.children()) { + visit(child); + } + + if (Element.isElement(node)) { + // Exiting an element: remove it from the ancestor filter. + filter.remove(node); + } + }; + + visit(root); + + return Cascade.of(root, device, selectors, rules, entries); + }); + } + + // root and device used to build the cascade. These are only kept for debugging + // purpose, since they are not really used after the cascade is built. private readonly _root: Document | Shadow; private readonly _device: Device; + // Selector map of all selectors in the User Agent style sheet and the root style sheets. private readonly _selectors: SelectorMap; - private readonly _rules = RuleTree.empty(); - - private readonly _entries = Cache.empty< - Element, - Cache - >(); - - private constructor(root: Document | Shadow, device: Device) { + // Rule tree, built incrementally upon need. + private readonly _rules: RuleTree; + + // Map from elements (and contexts) to nodes in the rule tree. + private readonly _entries: Cache>; + + private constructor( + root: Document | Shadow, + device: Device, + selectors: SelectorMap, + rules: RuleTree, + entries: Cache>, + ) { this._root = root; this._device = device; - this._selectors = SelectorMap.from([UserAgent, ...root.style], device); - - // Perform a baseline cascade with an empty context to benefit from ancestor - // filtering. As getting style information with an empty context will be the - // common case, we benefit a lot from pre-computing this style information - // with an ancestor filter applied. - - const context = Context.empty(); - - const filter = AncestorFilter.empty(); - - const visit = (node: Node): void => { - if (Element.isElement(node)) { - // Since we are traversing the full DOM tree and maintaining our own - // ancestor filter on the way, use the simple #add. - - // Entering an element: add it to the rule tree, and to the ancestor filter. - this.add(node, context, Option.of(filter)); - filter.add(node); - } - - for (const child of node.children()) { - visit(child); - } - - if (Element.isElement(node)) { - // Exiting an element: remove it from the ancestor filter. - filter.remove(node); - } - }; - - visit(root); + // Build a selector map with the User Agent style sheet, and the root style sheets. + this._selectors = selectors; + this._rules = rules; + this._entries = entries; } /** @@ -104,7 +153,13 @@ export class Cascade implements Serializable { context: Context = Context.empty(), filter: Option = None, ): RuleTree.Node { - return this._rules.add(this._selectors.get(element, context, filter)); + // We want to update the cache and therefore rely on the ifMissing mechanism + // of its getter. + return this._entries + .get(element, Cache.empty) + .get(context, () => + this._rules.add(this._selectors.get(element, context, filter)), + ); } /** diff --git a/packages/alfa-cascade/test/cascade.spec.ts b/packages/alfa-cascade/test/cascade.spec.ts new file mode 100644 index 0000000000..39b513bffb --- /dev/null +++ b/packages/alfa-cascade/test/cascade.spec.ts @@ -0,0 +1,7 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Cascade } from "../src"; + +test("foo", (t) => { + t.deepEqual(1, 1); +}); diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 98fd124c48..42d1e8cae1 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -18,6 +18,7 @@ "src/selector-map.ts", "src/user-agent.ts", "test/ancestor-filter.spec.tsx", + "test/cascade.spec.ts", "test/rule-tree.spec.ts", "test/selector-map.spec.tsx" ], diff --git a/packages/alfa-rules/src/sia-r83/rule.ts b/packages/alfa-rules/src/sia-r83/rule.ts index 21a48d7f51..51e0bae3cb 100644 --- a/packages/alfa-rules/src/sia-r83/rule.ts +++ b/packages/alfa-rules/src/sia-r83/rule.ts @@ -503,7 +503,7 @@ function getUsedMediaRules( // Get all nodes (style rules) in the RuleTree that affect the element; // for each of these rules, get all ancestor media rules in the CSS tree. return ancestorsInRuleTree( - Cascade.of(root, device).get(element, context), + Cascade.from(root, device).get(element, context), ).flatMap((node) => ancestorMediaRules(node.block.rule)); } diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index ebac1742c2..f2bd18f69c 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -319,7 +319,7 @@ export namespace Style { const root = element.root(); if (Document.isDocument(root) || Shadow.isShadow(root)) { - const cascade = Cascade.of(root, device); + const cascade = Cascade.from(root, device); // Walk up the cascade, starting from the node associated to the // element, and gather all declarations met on the way. From bac94f8ab6ab24646e28414b582f2025d909808f Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 10:14:54 +0100 Subject: [PATCH 17/26] Revert "Rename Cascade.of into Cascade.from" This reverts commit 04a36b2fa6b90c440996422a6f90856b2345686a. --- .changeset/stale-crews-wonder.md | 7 - docs/review/api/alfa-cascade.api.md | 64 +++++---- packages/alfa-cascade/src/cascade.ts | 147 +++++++-------------- packages/alfa-cascade/test/cascade.spec.ts | 7 - packages/alfa-cascade/tsconfig.json | 1 - packages/alfa-rules/src/sia-r83/rule.ts | 2 +- packages/alfa-style/src/style.ts | 2 +- 7 files changed, 89 insertions(+), 141 deletions(-) delete mode 100644 .changeset/stale-crews-wonder.md delete mode 100644 packages/alfa-cascade/test/cascade.spec.ts diff --git a/.changeset/stale-crews-wonder.md b/.changeset/stale-crews-wonder.md deleted file mode 100644 index c94c4058a4..0000000000 --- a/.changeset/stale-crews-wonder.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@siteimprove/alfa-cascade": minor ---- - -**Breaking:** `Cascade.of` has been renamed `Cascade.from`. - -This matches the functinolatiy which is doing significant amount of work to prepare the cascade, and therefore aligns better with the rest of the naming scheme. diff --git a/docs/review/api/alfa-cascade.api.md b/docs/review/api/alfa-cascade.api.md index b9e0017f3a..407bd13c3d 100644 --- a/docs/review/api/alfa-cascade.api.md +++ b/docs/review/api/alfa-cascade.api.md @@ -4,17 +4,11 @@ ```ts -import { Array as Array_2 } from '@siteimprove/alfa-array'; -import { Cache } from '@siteimprove/alfa-cache'; -import { Comparer } from '@siteimprove/alfa-comparable'; -import { Complex } from '@siteimprove/alfa-selector'; -import { Compound } from '@siteimprove/alfa-selector'; import { Context } from '@siteimprove/alfa-selector'; import { Declaration } from '@siteimprove/alfa-dom'; import { Device } from '@siteimprove/alfa-device'; import { Document } from '@siteimprove/alfa-dom'; import { Element } from '@siteimprove/alfa-dom'; -import type { Equatable } from '@siteimprove/alfa-equatable'; import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; import * as json from '@siteimprove/alfa-json'; import { Option } from '@siteimprove/alfa-option'; @@ -23,18 +17,15 @@ import { Selector } from '@siteimprove/alfa-selector'; import { Serializable } from '@siteimprove/alfa-json'; import { Shadow } from '@siteimprove/alfa-dom'; import { Sheet } from '@siteimprove/alfa-dom'; -import { Simple } from '@siteimprove/alfa-selector'; -import { Specificity } from '@siteimprove/alfa-selector/src/specificity'; -import { StyleRule } from '@siteimprove/alfa-dom'; // @public (undocumented) export class Cascade implements Serializable { - static from(root: Document | Shadow, device: Device): Cascade; - get(element: Element, context?: Context): RuleTree.Node; - // Warning: (ae-forgotten-export) The symbol "SelectorMap" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "AncestorFilter" needs to be exported by the entry point index.d.ts // - // @internal - static of(root: Document | Shadow, device: Device, selectors: SelectorMap, rules: RuleTree, entries: Cache>): Cascade; + // (undocumented) + get(element: Element, context?: Context, filter?: Option): RuleTree.Node; + // (undocumented) + static of(node: Document | Shadow, device: Device): Cascade; // (undocumented) toJSON(): Cascade.JSON; } @@ -51,6 +42,8 @@ export namespace Cascade { root: Document.JSON | Shadow.JSON; // (undocumented) rules: RuleTree.JSON; + // Warning: (ae-forgotten-export) The symbol "SelectorMap" needs to be exported by the entry point index.d.ts + // // (undocumented) selectors: SelectorMap.JSON; } @@ -58,10 +51,8 @@ export namespace Cascade { // @public export class RuleTree implements Serializable { - // Warning: (ae-forgotten-export) The symbol "Block" needs to be exported by the entry point index.d.ts - // // @internal - add(rules: Iterable_2): RuleTree.Node; + add(rules: Iterable_2): RuleTree.Node; // (undocumented) static empty(): RuleTree; // (undocumented) @@ -70,25 +61,52 @@ export class RuleTree implements Serializable { // @public (undocumented) export namespace RuleTree { + // @internal + export interface Item { + // (undocumented) + declarations: Iterable_2; + // (undocumented) + rule: Rule; + // (undocumented) + selector: Selector; + } + // (undocumented) + export namespace Item { + // (undocumented) + export interface JSON { + // (undocumented) + [key: string]: json.JSON; + // (undocumented) + declarations: Array; + // (undocumented) + rule: Rule.JSON; + // (undocumented) + selector: Selector.JSON; + } + } // (undocumented) export type JSON = Array; // (undocumented) export class Node implements Serializable { // @internal - add(block: Block): Node; + add(item: Item): Node; // (undocumented) ancestors(): Iterable_2; // (undocumented) - get block(): Block; - // (undocumented) get children(): Array; // (undocumented) + get declarations(): Iterable_2; + // (undocumented) inclusiveAncestors(): Iterable_2; // (undocumented) - static of(block: Block, children: Array, parent: Option): Node; + static of({ rule, selector, declarations }: Item, children: Array, parent: Option): Node; // (undocumented) get parent(): Option; // (undocumented) + get rule(): Rule; + // (undocumented) + get selector(): Selector; + // (undocumented) toJSON(): Node.JSON; } // (undocumented) @@ -98,9 +116,9 @@ export namespace RuleTree { // (undocumented) [key: string]: json.JSON; // (undocumented) - block: Block.JSON; - // (undocumented) children: Array; + // (undocumented) + item: Item.JSON; } } } diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 4d90319626..4c785ec273 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -16,7 +16,7 @@ import { UserAgent } from "./user-agent"; * {@link https://drafts.csswg.org/css-cascade-5/} * * @remarks - * The cascade associates to each element a node into a rule tree. + * The cascade associate to each element a node into a rule tree. * A single rule tree is built for each document or shadow root. The cascade * lazily fills it upon need and caches the associated node for each element. * @@ -39,106 +39,57 @@ export class Cascade implements Serializable { Cache >(); - /** - * Unsafely create a cascade. - * - * @remarks - * This doesn't check coupling of data. This stores a cache that can still be - * accessed (and modified) from outside. This is only useful for writing tests - * without including the User Agent style sheet in the cascade. - * - * Do not use. Use Cascade.from() instead. Seriously. - * - * @internal - */ - public static of( - root: Document | Shadow, - device: Device, - selectors: SelectorMap, - rules: RuleTree, - entries: Cache>, - ): Cascade { - return new Cascade(root, device, selectors, rules, entries); + public static of(node: Document | Shadow, device: Device): Cascade { + return this._cascades + .get(node, Cache.empty) + .get(device, () => new Cascade(node, device)); } - /** - * Build a cascade. - * - * @privateRemarks - * This needs to be here rather than in the namespace because it also updates - * the global cascades cache. - */ - public static from(root: Document | Shadow, device: Device): Cascade { - // Caching all existing cascade since we need to maintain one for each - // document or shadow tree. - return this._cascades.get(root, Cache.empty).get(device, () => { - // Build a selector map with the User Agent style sheet, and the root style sheets. - const selectors = SelectorMap.from([UserAgent, ...root.style], device); - const rules = RuleTree.empty(); - const entries = Cache.empty>(); - - // Perform a baseline cascade with an empty context to benefit from ancestor - // filtering. As getting style information with an empty context will be the - // common case, we benefit a lot from pre-computing this style information - // with an ancestor filter applied. - const context = Context.empty(); - const filter = AncestorFilter.empty(); - - const visit = (node: Node): void => { - if (Element.isElement(node)) { - // Since we are traversing the full DOM tree and maintaining our own - // ancestor filter on the way, use the simple #add. - - // Entering an element: add it to the rule tree, and to the ancestor filter. - entries - .get(node, Cache.empty) - .get(context, () => - rules.add(selectors.get(node, context, Option.of(filter))), - ); - filter.add(node); - } - - for (const child of node.children()) { - visit(child); - } - - if (Element.isElement(node)) { - // Exiting an element: remove it from the ancestor filter. - filter.remove(node); - } - }; - - visit(root); - - return Cascade.of(root, device, selectors, rules, entries); - }); - } - - // root and device used to build the cascade. These are only kept for debugging - // purpose, since they are not really used after the cascade is built. private readonly _root: Document | Shadow; private readonly _device: Device; - // Selector map of all selectors in the User Agent style sheet and the root style sheets. private readonly _selectors: SelectorMap; - // Rule tree, built incrementally upon need. - private readonly _rules: RuleTree; - - // Map from elements (and contexts) to nodes in the rule tree. - private readonly _entries: Cache>; - - private constructor( - root: Document | Shadow, - device: Device, - selectors: SelectorMap, - rules: RuleTree, - entries: Cache>, - ) { + private readonly _rules = RuleTree.empty(); + + private readonly _entries = Cache.empty< + Element, + Cache + >(); + + private constructor(root: Document | Shadow, device: Device) { this._root = root; this._device = device; - // Build a selector map with the User Agent style sheet, and the root style sheets. - this._selectors = selectors; - this._rules = rules; - this._entries = entries; + this._selectors = SelectorMap.from([UserAgent, ...root.style], device); + + // Perform a baseline cascade with an empty context to benefit from ancestor + // filtering. As getting style information with an empty context will be the + // common case, we benefit a lot from pre-computing this style information + // with an ancestor filter applied. + + const context = Context.empty(); + + const filter = AncestorFilter.empty(); + + const visit = (node: Node): void => { + if (Element.isElement(node)) { + // Since we are traversing the full DOM tree and maintaining our own + // ancestor filter on the way, use the simple #add. + + // Entering an element: add it to the rule tree, and to the ancestor filter. + this.add(node, context, Option.of(filter)); + filter.add(node); + } + + for (const child of node.children()) { + visit(child); + } + + if (Element.isElement(node)) { + // Exiting an element: remove it from the ancestor filter. + filter.remove(node); + } + }; + + visit(root); } /** @@ -153,13 +104,7 @@ export class Cascade implements Serializable { context: Context = Context.empty(), filter: Option = None, ): RuleTree.Node { - // We want to update the cache and therefore rely on the ifMissing mechanism - // of its getter. - return this._entries - .get(element, Cache.empty) - .get(context, () => - this._rules.add(this._selectors.get(element, context, filter)), - ); + return this._rules.add(this._selectors.get(element, context, filter)); } /** diff --git a/packages/alfa-cascade/test/cascade.spec.ts b/packages/alfa-cascade/test/cascade.spec.ts deleted file mode 100644 index 39b513bffb..0000000000 --- a/packages/alfa-cascade/test/cascade.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test } from "@siteimprove/alfa-test"; - -import { Cascade } from "../src"; - -test("foo", (t) => { - t.deepEqual(1, 1); -}); diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 42d1e8cae1..98fd124c48 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -18,7 +18,6 @@ "src/selector-map.ts", "src/user-agent.ts", "test/ancestor-filter.spec.tsx", - "test/cascade.spec.ts", "test/rule-tree.spec.ts", "test/selector-map.spec.tsx" ], diff --git a/packages/alfa-rules/src/sia-r83/rule.ts b/packages/alfa-rules/src/sia-r83/rule.ts index 51e0bae3cb..21a48d7f51 100644 --- a/packages/alfa-rules/src/sia-r83/rule.ts +++ b/packages/alfa-rules/src/sia-r83/rule.ts @@ -503,7 +503,7 @@ function getUsedMediaRules( // Get all nodes (style rules) in the RuleTree that affect the element; // for each of these rules, get all ancestor media rules in the CSS tree. return ancestorsInRuleTree( - Cascade.from(root, device).get(element, context), + Cascade.of(root, device).get(element, context), ).flatMap((node) => ancestorMediaRules(node.block.rule)); } diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index f2bd18f69c..ebac1742c2 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -319,7 +319,7 @@ export namespace Style { const root = element.root(); if (Document.isDocument(root) || Shadow.isShadow(root)) { - const cascade = Cascade.from(root, device); + const cascade = Cascade.of(root, device); // Walk up the cascade, starting from the node associated to the // element, and gather all declarations met on the way. From 466fd243120f567efa46508f2cf2807f64cbb184 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 10:39:10 +0100 Subject: [PATCH 18/26] Rename Cascade.of to Cascade.from --- .changeset/fast-rules-leave.md | 7 +++ .changeset/moody-lizards-enjoy.md | 7 +++ packages/alfa-cascade/src/cascade.ts | 48 +++++++------------ packages/alfa-cascade/src/selector-map.ts | 18 +++---- packages/alfa-cascade/test/cascade.spec.tsx | 14 ++++++ .../alfa-cascade/test/selector-map.spec.tsx | 22 ++++----- packages/alfa-cascade/tsconfig.json | 1 + packages/alfa-rules/src/sia-r83/rule.ts | 2 +- packages/alfa-style/src/style.ts | 2 +- 9 files changed, 64 insertions(+), 57 deletions(-) create mode 100644 .changeset/fast-rules-leave.md create mode 100644 .changeset/moody-lizards-enjoy.md create mode 100644 packages/alfa-cascade/test/cascade.spec.tsx diff --git a/.changeset/fast-rules-leave.md b/.changeset/fast-rules-leave.md new file mode 100644 index 0000000000..c9bb9decb6 --- /dev/null +++ b/.changeset/fast-rules-leave.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Breaking:** `SelectorMap.#get` now requires an `AncestorFilter` rather than an `Option`. + +All actual use cases in the code are now passing an ancestor filter, so there is no need to wrap it as an option anymore. diff --git a/.changeset/moody-lizards-enjoy.md b/.changeset/moody-lizards-enjoy.md new file mode 100644 index 0000000000..a6df4ed1de --- /dev/null +++ b/.changeset/moody-lizards-enjoy.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Breaking:** `Cascade.of` has been renamed `Cascade.from`. + +This match better naming conventions in other packages, since it does perform some heavy computation before building the cascade. diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 4c785ec273..26e3203c35 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -2,7 +2,7 @@ import { Cache } from "@siteimprove/alfa-cache"; import { Device } from "@siteimprove/alfa-device"; import { Document, Element, Node, Shadow } from "@siteimprove/alfa-dom"; import { Serializable } from "@siteimprove/alfa-json"; -import { Option, None } from "@siteimprove/alfa-option"; +import { Option } from "@siteimprove/alfa-option"; import { Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; @@ -39,7 +39,7 @@ export class Cascade implements Serializable { Cache >(); - public static of(node: Document | Shadow, device: Device): Cascade { + public static from(node: Document | Shadow, device: Device): Cascade { return this._cascades .get(node, Cache.empty) .get(device, () => new Cascade(node, device)); @@ -71,11 +71,12 @@ export class Cascade implements Serializable { const visit = (node: Node): void => { if (Element.isElement(node)) { - // Since we are traversing the full DOM tree and maintaining our own - // ancestor filter on the way, use the simple #add. - // Entering an element: add it to the rule tree, and to the ancestor filter. - this.add(node, context, Option.of(filter)); + this._entries + .get(node, Cache.empty) + .get(context, () => + this._rules.add(this._selectors.get(node, context, filter)), + ); filter.add(node); } @@ -92,32 +93,18 @@ export class Cascade implements Serializable { visit(root); } - /** - * Add an element to the rule tree, returns the associated node. - * - * @remarks - * This is idempotent since the rule tree already checks physical identity - * of selectors upon insertion. However, calling it too often is bad for performance. - */ - private add( - element: Element, - context: Context = Context.empty(), - filter: Option = None, - ): RuleTree.Node { - return this._rules.add(this._selectors.get(element, context, filter)); - } - /** * Adds an element to the tree, with a custom ancestor filter. * * @remarks * A new ancestor filter is built and filled with the element's ancestors. - * When building the full cascade for a DOM tree, this is pointless and the - * faster #add should be used instead. When looking up the style of a single - * element, we assume shat the time spend going up the DOM tree to build an - * ancestor filter will be saved by matching less selectors. + * When building the full cascade for a DOM tree, this is pointless as we can + * just build the filter on the go during DOM tree traversal. When looking up + * the style of a single element, we assume shat the time spent going up the + * DOM tree to build an ancestor filter will be saved by matching fewer + * selectors. */ - private addAncestors(element: Element, context: Context): RuleTree.Node { + private add(element: Element, context: Context): RuleTree.Node { const filter = AncestorFilter.empty(); // Because CSS selectors do not cross shadow or document boundaries, // only get ancestors in the same tree. @@ -128,9 +115,7 @@ export class Cascade implements Serializable { .filter(Element.isElement) .forEach(filter.add.bind(filter)); - return this._rules.add( - this._selectors.get(element, context, Option.of(filter)), - ); + return this._rules.add(this._selectors.get(element, context, filter)); } /** @@ -143,8 +128,7 @@ export class Cascade implements Serializable { * * For other contexts, we assume that we will only need the style of a few elements * (e.g., when a link is focused we normally only need the style of the link itself). - * Therefore, pre-building the full tree is not worth the cost nor the saving we'd - * get with an ancestor filter. + * Therefore, pre-building the full tree is not worth the cost. */ public get( element: Element, @@ -155,7 +139,7 @@ export class Cascade implements Serializable { // If the entry hasn't been cached already, we assume we are querying // for a single element and pay the price of building its custom ancestor // filter, hopefully saving on the matching cost. - () => this.addAncestors(element, context), + () => this.add(element, context), ); } diff --git a/packages/alfa-cascade/src/selector-map.ts b/packages/alfa-cascade/src/selector-map.ts index 7972093a4b..c6546b37c0 100644 --- a/packages/alfa-cascade/src/selector-map.ts +++ b/packages/alfa-cascade/src/selector-map.ts @@ -105,20 +105,20 @@ export class SelectorMap implements Serializable { public *get( element: Element, context: Context, - filter: Option, + filter: AncestorFilter, ): Iterable { function* collect(candidates: Iterable): Iterable { for (const block of candidates) { + // If the ancestor filter can reject the selector, escape if ( - // If the ancestor filter can reject the selector, escape - filter.none( - (filter) => - isDescendantSelector(block.selector) && - filter.canReject(block.selector.left), - ) && - // otherwise, do the actual match. - block.selector.matches(element, context) + isDescendantSelector(block.selector) && + filter.canReject(block.selector.left) ) { + continue; + } + + // otherwise, do the actual match. + if (block.selector.matches(element, context)) { yield block; } } diff --git a/packages/alfa-cascade/test/cascade.spec.tsx b/packages/alfa-cascade/test/cascade.spec.tsx new file mode 100644 index 0000000000..65f9f8d585 --- /dev/null +++ b/packages/alfa-cascade/test/cascade.spec.tsx @@ -0,0 +1,14 @@ +import { Device } from "@siteimprove/alfa-device"; +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import { Cascade } from "../src"; + +test(".from() builds a cascade with the User Agent style sheet", (t) => { + const document = h.document([
Hello
]); + + const device = Device.standard(); + const cascade = Cascade.from(document, device); + + t.deepEqual(cascade.toJSON().rules.length, 58); +}); diff --git a/packages/alfa-cascade/test/selector-map.spec.tsx b/packages/alfa-cascade/test/selector-map.spec.tsx index f8b579ca7f..3d093ddc83 100644 --- a/packages/alfa-cascade/test/selector-map.spec.tsx +++ b/packages/alfa-cascade/test/selector-map.spec.tsx @@ -166,10 +166,12 @@ test("#get() returns all blocks whose selector match an element", (t) => { const element =
; - t.deepEqual(Array.toJSON([...map.get(element, Context.empty(), None)]), [ - ...blocks[0], - ...blocks[3], - ]); + t.deepEqual( + Array.toJSON([ + ...map.get(element, Context.empty(), AncestorFilter.empty()), + ]), + [...blocks[0], ...blocks[3]], + ); }); /** @@ -186,31 +188,23 @@ test("#get() respects ancestor filter", (t) => { const badFilter = AncestorFilter.empty(); badFilter.add(
); - badFilter.add(); const goodFilter = AncestorFilter.empty(); goodFilter.add(
); goodFilter.add(
); - goodFilter.add(); const target = Hello; const _ =
{target}
; // The filter is incorrect by not having the `
` and therefore rejects the `div span` rule. t.deepEqual( - Array.toJSON([...map.get(target, Context.empty(), Option.of(badFilter))]), + Array.toJSON([...map.get(target, Context.empty(), badFilter)]), blocks[0], ); // The filter is correct and let the `div span` rule be matched. t.deepEqual( - Array.toJSON([...map.get(target, Context.empty(), Option.of(goodFilter))]), - blocks[0].concat(blocks[1]), - ); - - // Without any filter, actual match is performed, and we get both rules. - t.deepEqual( - Array.toJSON([...map.get(target, Context.empty(), None)]), + Array.toJSON([...map.get(target, Context.empty(), goodFilter)]), blocks[0].concat(blocks[1]), ); }); diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index 98fd124c48..fbefbb6fa8 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -18,6 +18,7 @@ "src/selector-map.ts", "src/user-agent.ts", "test/ancestor-filter.spec.tsx", + "test/cascade.spec.tsx", "test/rule-tree.spec.ts", "test/selector-map.spec.tsx" ], diff --git a/packages/alfa-rules/src/sia-r83/rule.ts b/packages/alfa-rules/src/sia-r83/rule.ts index 21a48d7f51..51e0bae3cb 100644 --- a/packages/alfa-rules/src/sia-r83/rule.ts +++ b/packages/alfa-rules/src/sia-r83/rule.ts @@ -503,7 +503,7 @@ function getUsedMediaRules( // Get all nodes (style rules) in the RuleTree that affect the element; // for each of these rules, get all ancestor media rules in the CSS tree. return ancestorsInRuleTree( - Cascade.of(root, device).get(element, context), + Cascade.from(root, device).get(element, context), ).flatMap((node) => ancestorMediaRules(node.block.rule)); } diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index ebac1742c2..f2bd18f69c 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -319,7 +319,7 @@ export namespace Style { const root = element.root(); if (Document.isDocument(root) || Shadow.isShadow(root)) { - const cascade = Cascade.of(root, device); + const cascade = Cascade.from(root, device); // Walk up the cascade, starting from the node associated to the // element, and gather all declarations met on the way. From 96118f5208029e4f26b9064d1a68cf093e63b9db Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 11:02:38 +0100 Subject: [PATCH 19/26] Add tests --- packages/alfa-cascade/test/cascade.spec.tsx | 63 ++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/alfa-cascade/test/cascade.spec.tsx b/packages/alfa-cascade/test/cascade.spec.tsx index 65f9f8d585..06f538560e 100644 --- a/packages/alfa-cascade/test/cascade.spec.tsx +++ b/packages/alfa-cascade/test/cascade.spec.tsx @@ -1,14 +1,73 @@ +/// import { Device } from "@siteimprove/alfa-device"; import { h } from "@siteimprove/alfa-dom"; +import { Iterable } from "@siteimprove/alfa-iterable"; import { test } from "@siteimprove/alfa-test"; +import { Block } from "../src/block"; import { Cascade } from "../src"; +const device = Device.standard(); + test(".from() builds a cascade with the User Agent style sheet", (t) => { const document = h.document([
Hello
]); + const cascade = Cascade.from(document, device); + + // Even the "empty" cascade is pretty big due to the UA sheet. + // We only superficially check the selector map + t.deepEqual(cascade.toJSON().selectors.types.length, 96); + t.deepEqual(cascade.toJSON().selectors.other.length, 8); + + t.deepEqual(cascade.toJSON().selectors.ids.length, 0); + t.deepEqual(cascade.toJSON().selectors.classes.length, 0); +}); - const device = Device.standard(); +test(".get() returns the rule tree node of the given element", (t) => { + const div =
Hello
; + const rule = h.rule.style("div", { color: "red" }); + const document = h.document([div], [h.sheet([rule])]); const cascade = Cascade.from(document, device); - t.deepEqual(cascade.toJSON().rules.length, 58); + // The rule tree has 3 items on the way to the
: + // The fake root, the UA rule `div { display: block }`, and the document rule + // `div { color: red }` + // We thus just grab and check the path down from the fake root. + t.deepEqual(Iterable.toJSON(cascade.get(div).inclusiveAncestors())[2], { + // fake root + block: Block.empty().toJSON(), + children: [ + { + // UA rule + block: { + rule: { + type: "style", + selector: + "address, blockquote, center, div, figure, figcaption, footer, form, header, hr, legend, listing, main, p, plaintext, pre, xmp", + style: [{ name: "display", value: "block", important: false }], + }, + selector: { + type: "type", + specificity: { a: 0, b: 0, c: 1 }, + key: "div", + name: "div", + namespace: null, + }, + declarations: [{ name: "display", value: "block", important: false }], + precedence: { + origin: 1, + specificity: { a: 0, b: 0, c: 1 }, + order: 7, + }, + }, + children: [ + { + // Actual rule + // There are 58 rules in the UA sheet. + block: Block.from(rule, 58)[0][0].toJSON(), + children: [], + }, + ], + }, + ], + }); }); From 386ddda28d2a8028e82ef32245b8cd7f9a987e63 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 11:23:41 +0100 Subject: [PATCH 20/26] Clean up --- docs/review/api/alfa-cascade.api.md | 58 ++++++++++------------------- packages/alfa-cascade/package.json | 2 + packages/alfa-cascade/tsconfig.json | 2 + 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/docs/review/api/alfa-cascade.api.md b/docs/review/api/alfa-cascade.api.md index 407bd13c3d..7945773189 100644 --- a/docs/review/api/alfa-cascade.api.md +++ b/docs/review/api/alfa-cascade.api.md @@ -4,11 +4,16 @@ ```ts +import { Array as Array_2 } from '@siteimprove/alfa-array'; +import { Comparer } from '@siteimprove/alfa-comparable'; +import { Complex } from '@siteimprove/alfa-selector'; +import { Compound } from '@siteimprove/alfa-selector'; import { Context } from '@siteimprove/alfa-selector'; import { Declaration } from '@siteimprove/alfa-dom'; import { Device } from '@siteimprove/alfa-device'; import { Document } from '@siteimprove/alfa-dom'; import { Element } from '@siteimprove/alfa-dom'; +import type { Equatable } from '@siteimprove/alfa-equatable'; import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; import * as json from '@siteimprove/alfa-json'; import { Option } from '@siteimprove/alfa-option'; @@ -17,15 +22,15 @@ import { Selector } from '@siteimprove/alfa-selector'; import { Serializable } from '@siteimprove/alfa-json'; import { Shadow } from '@siteimprove/alfa-dom'; import { Sheet } from '@siteimprove/alfa-dom'; +import { Simple } from '@siteimprove/alfa-selector'; +import { Specificity } from '@siteimprove/alfa-selector/src/specificity'; +import { StyleRule } from '@siteimprove/alfa-dom'; // @public (undocumented) export class Cascade implements Serializable { - // Warning: (ae-forgotten-export) The symbol "AncestorFilter" needs to be exported by the entry point index.d.ts - // - // (undocumented) - get(element: Element, context?: Context, filter?: Option): RuleTree.Node; // (undocumented) - static of(node: Document | Shadow, device: Device): Cascade; + static from(node: Document | Shadow, device: Device): Cascade; + get(element: Element, context?: Context): RuleTree.Node; // (undocumented) toJSON(): Cascade.JSON; } @@ -51,8 +56,10 @@ export namespace Cascade { // @public export class RuleTree implements Serializable { + // Warning: (ae-forgotten-export) The symbol "Block" needs to be exported by the entry point index.d.ts + // // @internal - add(rules: Iterable_2): RuleTree.Node; + add(rules: Iterable_2): RuleTree.Node; // (undocumented) static empty(): RuleTree; // (undocumented) @@ -61,52 +68,25 @@ export class RuleTree implements Serializable { // @public (undocumented) export namespace RuleTree { - // @internal - export interface Item { - // (undocumented) - declarations: Iterable_2; - // (undocumented) - rule: Rule; - // (undocumented) - selector: Selector; - } - // (undocumented) - export namespace Item { - // (undocumented) - export interface JSON { - // (undocumented) - [key: string]: json.JSON; - // (undocumented) - declarations: Array; - // (undocumented) - rule: Rule.JSON; - // (undocumented) - selector: Selector.JSON; - } - } // (undocumented) export type JSON = Array; // (undocumented) export class Node implements Serializable { // @internal - add(item: Item): Node; + add(block: Block): Node; // (undocumented) ancestors(): Iterable_2; // (undocumented) - get children(): Array; + get block(): Block; // (undocumented) - get declarations(): Iterable_2; + get children(): Array; // (undocumented) inclusiveAncestors(): Iterable_2; // (undocumented) - static of({ rule, selector, declarations }: Item, children: Array, parent: Option): Node; + static of(block: Block, children: Array, parent: Option): Node; // (undocumented) get parent(): Option; // (undocumented) - get rule(): Rule; - // (undocumented) - get selector(): Selector; - // (undocumented) toJSON(): Node.JSON; } // (undocumented) @@ -116,9 +96,9 @@ export namespace RuleTree { // (undocumented) [key: string]: json.JSON; // (undocumented) - children: Array; + block: Block.JSON; // (undocumented) - item: Item.JSON; + children: Array; } } } diff --git a/packages/alfa-cascade/package.json b/packages/alfa-cascade/package.json index e254083a00..cac3596ed2 100644 --- a/packages/alfa-cascade/package.json +++ b/packages/alfa-cascade/package.json @@ -18,11 +18,13 @@ "src/**/*.d.ts" ], "dependencies": { + "@siteimprove/alfa-array": "workspace:^0.70.0", "@siteimprove/alfa-cache": "workspace:^0.70.0", "@siteimprove/alfa-comparable": "workspace:^0.70.0", "@siteimprove/alfa-css": "workspace:^0.70.0", "@siteimprove/alfa-device": "workspace:^0.70.0", "@siteimprove/alfa-dom": "workspace:^0.70.0", + "@siteimprove/alfa-equatable": "workspace:^0.70.0", "@siteimprove/alfa-iterable": "workspace:^0.70.0", "@siteimprove/alfa-json": "workspace:^0.70.0", "@siteimprove/alfa-media": "workspace:^0.70.0", diff --git a/packages/alfa-cascade/tsconfig.json b/packages/alfa-cascade/tsconfig.json index fbefbb6fa8..7b80a38f0d 100644 --- a/packages/alfa-cascade/tsconfig.json +++ b/packages/alfa-cascade/tsconfig.json @@ -23,11 +23,13 @@ "test/selector-map.spec.tsx" ], "references": [ + { "path": "../alfa-array" }, { "path": "../alfa-cache" }, { "path": "../alfa-comparable" }, { "path": "../alfa-css" }, { "path": "../alfa-device" }, { "path": "../alfa-dom" }, + { "path": "../alfa-equatable" }, { "path": "../alfa-iterable" }, { "path": "../alfa-json" }, { "path": "../alfa-media" }, From 5ea2d873f3d189a354d5c34268ff681cedb6395b Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 13:01:14 +0100 Subject: [PATCH 21/26] Typos --- .changeset/moody-lizards-enjoy.md | 2 +- .changeset/selfish-elephants-float.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/moody-lizards-enjoy.md b/.changeset/moody-lizards-enjoy.md index a6df4ed1de..477d6b527c 100644 --- a/.changeset/moody-lizards-enjoy.md +++ b/.changeset/moody-lizards-enjoy.md @@ -4,4 +4,4 @@ **Breaking:** `Cascade.of` has been renamed `Cascade.from`. -This match better naming conventions in other packages, since it does perform some heavy computation before building the cascade. +This matches better naming conventions in other packages, since it does perform some heavy computation before building the cascade. diff --git a/.changeset/selfish-elephants-float.md b/.changeset/selfish-elephants-float.md index 193f9bc2c0..5023fa6ed4 100644 --- a/.changeset/selfish-elephants-float.md +++ b/.changeset/selfish-elephants-float.md @@ -4,4 +4,4 @@ **Removed:** `AncestorFilter#match` has been made internal. -se `!ANcestorFilter.canReject` instead, which is having less assumptions. +Use `!AncestorFilter.canReject` instead, which is having fewer assumptions. From 0add3ea2517f13ef24559da574b1f2122e3aa5ea Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 13:02:50 +0100 Subject: [PATCH 22/26] Update package --- packages/alfa-comparable/package.json | 2 +- yarn.lock | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/alfa-comparable/package.json b/packages/alfa-comparable/package.json index 2571e488d6..e12bd41165 100644 --- a/packages/alfa-comparable/package.json +++ b/packages/alfa-comparable/package.json @@ -21,7 +21,7 @@ "@siteimprove/alfa-refinement": "workspace:^0.71.1" }, "devDependencies": { - "@siteimprove/alfa-test": "workspace:^0.70.0" + "@siteimprove/alfa-test": "workspace:^0.71.1" }, "publishConfig": { "access": "public", diff --git a/yarn.lock b/yarn.lock index c7263d5704..e02262266c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -944,11 +944,13 @@ __metadata: version: 0.0.0-use.local resolution: "@siteimprove/alfa-cascade@workspace:packages/alfa-cascade" dependencies: + "@siteimprove/alfa-array": "workspace:^0.71.1" "@siteimprove/alfa-cache": "workspace:^0.71.1" "@siteimprove/alfa-comparable": "workspace:^0.71.1" "@siteimprove/alfa-css": "workspace:^0.71.1" "@siteimprove/alfa-device": "workspace:^0.71.1" "@siteimprove/alfa-dom": "workspace:^0.71.1" + "@siteimprove/alfa-equatable": "workspace:^0.71.1" "@siteimprove/alfa-iterable": "workspace:^0.71.1" "@siteimprove/alfa-json": "workspace:^0.71.1" "@siteimprove/alfa-media": "workspace:^0.71.1" @@ -994,6 +996,7 @@ __metadata: resolution: "@siteimprove/alfa-comparable@workspace:packages/alfa-comparable" dependencies: "@siteimprove/alfa-refinement": "workspace:^0.71.1" + "@siteimprove/alfa-test": "workspace:^0.71.1" languageName: unknown linkType: soft @@ -1637,6 +1640,7 @@ __metadata: dependencies: "@siteimprove/alfa-array": "workspace:^0.71.1" "@siteimprove/alfa-cache": "workspace:^0.71.1" + "@siteimprove/alfa-comparable": "workspace:^0.71.1" "@siteimprove/alfa-css": "workspace:^0.71.1" "@siteimprove/alfa-dom": "workspace:^0.71.1" "@siteimprove/alfa-equatable": "workspace:^0.71.1" From e2dda79e22f0ac0b32fb5992b99490b5afdd5d20 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 13:23:59 +0100 Subject: [PATCH 23/26] Add changeset --- .changeset/tender-starfishes-drum.md | 7 +++++++ packages/alfa-cascade/test/cascade.spec.tsx | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-starfishes-drum.md diff --git a/.changeset/tender-starfishes-drum.md b/.changeset/tender-starfishes-drum.md new file mode 100644 index 0000000000..74f1fb7159 --- /dev/null +++ b/.changeset/tender-starfishes-drum.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cascade": minor +--- + +**Breaking:** Data in Rule tre nodes is now wrapped in a `Block` object that need to be opened. + +That is, replace, e.g., `node.declarations` with `node.block.declarations` to access the declarations associated to a node in the rule tree, and so forth for other data. diff --git a/packages/alfa-cascade/test/cascade.spec.tsx b/packages/alfa-cascade/test/cascade.spec.tsx index 06f538560e..7ae48fdb0f 100644 --- a/packages/alfa-cascade/test/cascade.spec.tsx +++ b/packages/alfa-cascade/test/cascade.spec.tsx @@ -1,4 +1,3 @@ -/// import { Device } from "@siteimprove/alfa-device"; import { h } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; From 7d65b78e37068347aa1e71511be2415dc7ae6424 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 13:24:37 +0100 Subject: [PATCH 24/26] Clean up --- packages/alfa-cascade/src/cascade.ts | 1 - packages/alfa-cascade/test/selector-map.spec.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/alfa-cascade/src/cascade.ts b/packages/alfa-cascade/src/cascade.ts index 26e3203c35..ea13b8d3a9 100644 --- a/packages/alfa-cascade/src/cascade.ts +++ b/packages/alfa-cascade/src/cascade.ts @@ -2,7 +2,6 @@ import { Cache } from "@siteimprove/alfa-cache"; import { Device } from "@siteimprove/alfa-device"; import { Document, Element, Node, Shadow } from "@siteimprove/alfa-dom"; import { Serializable } from "@siteimprove/alfa-json"; -import { Option } from "@siteimprove/alfa-option"; import { Context } from "@siteimprove/alfa-selector"; import * as json from "@siteimprove/alfa-json"; diff --git a/packages/alfa-cascade/test/selector-map.spec.tsx b/packages/alfa-cascade/test/selector-map.spec.tsx index 3d093ddc83..fa374eb217 100644 --- a/packages/alfa-cascade/test/selector-map.spec.tsx +++ b/packages/alfa-cascade/test/selector-map.spec.tsx @@ -1,7 +1,6 @@ import { Array } from "@siteimprove/alfa-array"; import { Device } from "@siteimprove/alfa-device"; import { h, StyleRule } from "@siteimprove/alfa-dom"; -import { None, Option } from "@siteimprove/alfa-option"; import { Context } from "@siteimprove/alfa-selector"; import { test } from "@siteimprove/alfa-test"; import { AncestorFilter } from "../src/ancestor-filter"; From dab3ebe0923df1427880641b7a978dc30b743b42 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:39:16 +0000 Subject: [PATCH 25/26] Extract API --- docs/review/api/alfa-dom.api.md | 2 ++ docs/review/api/alfa-selector.api.md | 49 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/docs/review/api/alfa-dom.api.md b/docs/review/api/alfa-dom.api.md index 9f2903a221..dfbb9e0a8e 100644 --- a/docs/review/api/alfa-dom.api.md +++ b/docs/review/api/alfa-dom.api.md @@ -459,6 +459,8 @@ export namespace h { // (undocumented) export function fontFace(declarations: Array | Record): FontFaceRule; // (undocumented) + export function importRule(url: string, sheet: Sheet, condition?: string): ImportRule; + // (undocumented) export function keyframe(key: string, declarations: Array | Record): KeyframeRule; // (undocumented) export function keyframes(name: string, rules: Array): KeyframesRule; diff --git a/docs/review/api/alfa-selector.api.md b/docs/review/api/alfa-selector.api.md index 052ae4790e..a02bb777c1 100644 --- a/docs/review/api/alfa-selector.api.md +++ b/docs/review/api/alfa-selector.api.md @@ -5,6 +5,7 @@ ```ts import { Array as Array_2 } from '@siteimprove/alfa-array'; +import { Comparer } from '@siteimprove/alfa-comparable'; import { Element } from '@siteimprove/alfa-dom'; import { Equatable } from '@siteimprove/alfa-equatable'; import { Hash } from '@siteimprove/alfa-hash'; @@ -494,6 +495,54 @@ export namespace Simple { parse: (parseSelector: Thunk>) => Parser, Simple, string, []>; } +// @public (undocumented) +export class Specificity implements Serializable, Equatable, Hashable { + // (undocumented) + get a(): number; + // (undocumented) + get b(): number; + // (undocumented) + get c(): number; + // (undocumented) + static empty(): Specificity; + // (undocumented) + equals(value: Specificity): boolean; + // (undocumented) + equals(value: unknown): value is this; + // (undocumented) + hash(hash: Hash): void; + // (undocumented) + static of(a: number, b: number, c: number): Specificity; + // (undocumented) + toJSON(): Specificity.JSON; + // (undocumented) + toString(): string; + // (undocumented) + get value(): number; +} + +// @public (undocumented) +export namespace Specificity { + export function isSpecificity(value: unknown): value is Specificity; + // (undocumented) + export interface JSON { + // (undocumented) + [key: string]: json.JSON; + // (undocumented) + a: number; + // (undocumented) + b: number; + // (undocumented) + c: number; + } + // (undocumented) + export function max(...specificities: ReadonlyArray): Specificity; + // (undocumented) + export function sum(...specificities: ReadonlyArray): Specificity; + const // (undocumented) + compare: Comparer; +} + // @public (undocumented) export class Type extends WithName<"type"> { // (undocumented) From f7233f2408d5a865f38ac339b73ce8449729119a Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 20 Dec 2023 13:56:31 +0100 Subject: [PATCH 26/26] Add release tag --- packages/alfa-comparable/src/comparer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/alfa-comparable/src/comparer.ts b/packages/alfa-comparable/src/comparer.ts index 8b52e377bb..dd428c5ab3 100644 --- a/packages/alfa-comparable/src/comparer.ts +++ b/packages/alfa-comparable/src/comparer.ts @@ -11,6 +11,8 @@ export type Comparer = []> = ( /** * Turns a tuple of types into a tuple of comparer over these types. + * + * @public */ export type TupleComparer> = T extends [ infer Head,