Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add style attribute and importance to cascade sort #1550

Merged
merged 14 commits into from
Jan 16, 2024
8 changes: 8 additions & 0 deletions .changeset/kind-singers-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@siteimprove/alfa-cascade": minor
---

**Added:** Cascade now handle importance of declarations, and `style` attribute.

This should have no impact on regular usage of `Style.from()` but may affect code trying to use the cascade directly.
Most notably, the internal rule tree `Block` can now come either from a rule or an element. Therefore, `Block.rule` and `Bolkc.selector` may now be `null`.
2 changes: 1 addition & 1 deletion docs/review/api/alfa-cascade.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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 { 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';
Expand Down
6 changes: 0 additions & 6 deletions docs/review/api/alfa-style.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,6 @@ export namespace Shorthands {
export type Property = typeof shortHands;
}

// Warning: (ae-internal-missing-underscore) The name "shouldOverride" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export function shouldOverride<T>(previous: Option<Value<T>>, next: Declaration): boolean;

// @public (undocumented)
export class Style implements Serializable<Style.JSON> {
// Warning: (ae-forgotten-export) The symbol "Name" needs to be exported by the entry point index.d.ts
Expand All @@ -328,7 +323,6 @@ export class Style implements Serializable<Style.JSON> {
inherited<N extends Name>(name: N): Value<Style.Inherited<N>>;
// (undocumented)
initial<N extends Name>(name: N, source?: Option<Declaration>): Value<Style.Initial<N>>;
// (undocumented)
static of(styleDeclarations: Iterable_2<Declaration>, device: Device, parent?: Option<Style>): Style;
// (undocumented)
get parent(): Style;
Expand Down
204 changes: 153 additions & 51 deletions packages/alfa-cascade/src/block.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Array } from "@siteimprove/alfa-array";
import { type Comparer, Comparison } from "@siteimprove/alfa-comparable";
import { Lexer } from "@siteimprove/alfa-css";
import { Declaration, h, Rule, StyleRule } from "@siteimprove/alfa-dom";
import type { Equatable } from "@siteimprove/alfa-equatable";
import {
type Block as StyleBlock,
Declaration,
Element,
h,
Rule,
StyleRule,
} from "@siteimprove/alfa-dom";
import { Equatable } from "@siteimprove/alfa-equatable";
import { Iterable } from "@siteimprove/alfa-iterable";
import { Serializable } from "@siteimprove/alfa-json";
import { None } from "@siteimprove/alfa-option";
Expand Down Expand Up @@ -35,9 +42,7 @@ import { UserAgent } from "./user-agent";
*
* @internal
*/
export class Block<
S extends Compound | Complex | Simple = Compound | Complex | Simple,
>
export class Block<S extends Element | Block.Source = Element | Block.Source>
implements Equatable, Serializable<Block.JSON<S>>
{
/**
Expand All @@ -46,25 +51,25 @@ export class Block<
* @remarks
* This does not validate coupling of the data. Prefer using Block.from()
*/
public static of<
S extends Compound | Complex | Simple = Compound | Complex | Simple,
>(
rule: StyleRule,
selector: S,
public static of<S extends Element | Block.Source = Element | Block.Source>(
source: S,
declarations: Iterable<Declaration>,
precedence: Precedence,
): Block<S> {
return new Block(rule, selector, Array.from(declarations), precedence);
return new Block(source, Array.from(declarations), precedence);
}

private static _empty = new Block(
h.rule.style("*", []),
Universal.of(None),
{
rule: h.rule.style("*", []),
selector: Universal.of(None),
},
[],
{
origin: Origin.UserAgent,
origin: Origin.NormalUserAgent,
isElementAttached: false,
specificity: Specificity.empty(),
order: Infinity,
order: -Infinity,
},
);
/**
Expand All @@ -74,31 +79,63 @@ export class Block<
return this._empty;
}

private readonly _rule: StyleRule;
private readonly _selector: S;
// These could be Options instead.
// However, these (especially the selector) are used on hot path when
// resolving cascade. Having them nullable, and encoding the nullability
// in the type, allow for direct access without the small overhead of Options.
private readonly _rule: S extends Block.Source ? StyleRule : null;
private readonly _selector: S extends Block.Source
? Compound | Complex | Simple
: null;
private readonly _owner: S extends Element ? Element : null;
private readonly _declarations: Array<Declaration>;
private readonly _precedence: Precedence;

constructor(
rule: StyleRule,
selector: S,
source: S,
declarations: Array<Declaration>,
precedence: Precedence,
) {
this._rule = rule;
this._selector = selector;
if (Element.isElement(source)) {
this._rule = null as S extends Block.Source ? StyleRule : null;
this._selector = null as S extends Block.Source
? Compound | Complex | Simple
: null;
this._owner = source as unknown as S extends Element ? Element : null;
} else {
this._rule = source.rule as S extends Block.Source ? StyleRule : null;
this._selector = source.selector as S extends Block.Source
? Compound | Complex | Simple
: null;
this._owner = null as S extends Element ? Element : null;
}
this._declarations = declarations;
this._precedence = precedence;
}

public get rule(): StyleRule {
/** @public (knip) */
public get source(): S {
return this._owner !== null
? (this._owner as unknown as S)
: // By construction if owner is unset, then rule and selector are set.
({ rule: this._rule, selector: this._selector } as S);
}

public get rule(): S extends Block.Source ? StyleRule : null {
return this._rule;
}

public get selector(): S {
public get selector(): S extends Block.Source
? Compound | Complex | Simple
: null {
return this._selector;
}

/** @public (knip) */
public get owner(): S extends Element ? Element : null {
return this._owner;
}

public get declarations(): Iterable<Declaration> {
return this._declarations;
}
Expand All @@ -114,8 +151,9 @@ export class Block<
public equals(value: unknown): boolean {
return (
value instanceof Block &&
this._rule.equals(value._rule) &&
this._selector.equals(value._selector) &&
Equatable.equals(value._rule, this._rule) &&
Equatable.equals(value._selector, this._selector) &&
Equatable.equals(value._owner, this._owner) &&
Array.equals(value._declarations, this._declarations) &&
Precedence.compare(value._precedence, this._precedence) ===
Comparison.Equal
Expand All @@ -124,8 +162,17 @@ export class Block<

public toJSON(): Block.JSON<S> {
return {
rule: this._rule.toJSON(),
selector: Serializable.toJSON(this._selector),
source: (Element.isElement(this._owner)
? this._owner.toJSON()
: {
rule: this._rule!.toJSON(),
selector: Serializable.toJSON(this._selector),
}) as S extends Element
? Element.JSON
: {
rule: Rule.JSON;
selector: Serializable.ToJSON<S>;
},
declarations: Array.toJSON(this._declarations),
precedence: Precedence.toJSON(this._precedence),
};
Expand All @@ -135,16 +182,26 @@ export class Block<
* @internal
*/
export namespace Block {
export interface JSON<
S extends Compound | Complex | Simple = Compound | Complex | Simple,
> {
export interface JSON<S extends Element | Source = Element | Source> {
[key: string]: json.JSON;
rule: Rule.JSON;
selector: Serializable.ToJSON<S>;
source: S extends Element
? Element.JSON
: {
rule: Rule.JSON;
selector: Serializable.ToJSON<S>;
};
declarations: Array<Declaration.JSON>;
precedence: Precedence.JSON;
}

/**
* @internal
*/
export interface Source {
rule: StyleRule;
selector: Compound | Complex | Simple;
}

/**
* Build Blocks from a style rule. Returns the last order used, that is unchanged
* if selector couldn't be parsed, increased by 1 otherwise.
Expand All @@ -153,35 +210,80 @@ export namespace Block {
* 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.
* A single rule creates more than one block.
* * Declarations inside the rule are split by importance.
* * 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 either declaring
* the exact same declarations, or non-conflicting ones (due to importance), they can
* share the exact same order.
*/
export function from(rule: StyleRule, order: number): [Array<Block>, number] {
let blocks = [];
export function from(
rule: StyleRule,
order: number,
): [Array<Block<Source>>, number] {
let blocks: Array<Block<Source>> = [];

for (const [_, selectors] of Selector.parse(Lexer.lex(rule.selector))) {
const origin = rule.owner.includes(UserAgent)
? Origin.UserAgent
: Origin.Author;

// The selector was parsed successfully, so blocks will be created, and we need to update order.
order++;

for (const selector of selectors) {
blocks.push(
Block.of(rule, selector, rule.style, {
origin,
order,
specificity: selector.specificity,
}),
);
for (const [importance, declarations] of Iterable.groupBy(
rule.style.declarations,
(declaration) => declaration.important,
)) {
const origin = rule.owner.includes(UserAgent)
? importance
? Origin.ImportantUserAgent
: Origin.NormalUserAgent
: importance
? Origin.ImportantAuthor
: Origin.NormalAuthor;

for (const selector of selectors) {
blocks.push(
Block.of({ rule, selector }, declarations, {
origin,
isElementAttached: false,
order,
specificity: selector.specificity,
}),
);
}
}
}
return [blocks, order];
}

/**
* Turns the style attribute of an element into blocks (one for important
* declarations, one for normal declarations).
* @param element
*/
export function fromStyle(element: Element): Iterable<Block> {
return element.style
.map((style) =>
Iterable.map(
Iterable.groupBy(
style.declarations,
(declaration) => declaration.important,
),
([importance, declarations]) =>
Block.of(element, declarations, {
origin: importance ? Origin.ImportantAuthor : Origin.NormalAuthor,
isElementAttached: true,
specificity: Specificity.empty(),
// Since style attribute trumps style rules in the cascade sort,
// and there is at most one style attribute per element,
// the order never matters.
order: -1,
}),
),
)
.getOr<Iterable<Block>>([]);
}

export const compare: Comparer<Block> = (a, b) =>
Precedence.compare(a.precedence, b.precedence);
}
16 changes: 14 additions & 2 deletions packages/alfa-cascade/src/cascade.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Cache } from "@siteimprove/alfa-cache";
import { Device } from "@siteimprove/alfa-device";
import { Document, Element, Node, Shadow } from "@siteimprove/alfa-dom";
import { Iterable } from "@siteimprove/alfa-iterable";
import { Serializable } from "@siteimprove/alfa-json";
import { Context } from "@siteimprove/alfa-selector";

import * as json from "@siteimprove/alfa-json";

import { AncestorFilter } from "./ancestor-filter";
import { Block } from "./block";
import { RuleTree } from "./rule-tree";
import { SelectorMap } from "./selector-map";
import { UserAgent } from "./user-agent";
Expand Down Expand Up @@ -74,7 +76,12 @@ export class Cascade implements Serializable {
this._entries
.get(node, Cache.empty)
.get(context, () =>
this._rules.add(this._selectors.get(node, context, filter)),
this._rules.add(
Iterable.concat(
this._selectors.get(node, context, filter),
Block.fromStyle(node),
),
),
);
filter.add(node);
}
Expand Down Expand Up @@ -114,7 +121,12 @@ export class Cascade implements Serializable {
.filter(Element.isElement)
.forEach(filter.add.bind(filter));

return this._rules.add(this._selectors.get(element, context, filter));
return this._rules.add(
Iterable.concat(
this._selectors.get(element, context, filter),
Block.fromStyle(element),
),
);
}

/**
Expand Down
Loading