Skip to content

Commit

Permalink
Implement style resolution for stateful pseudo classes (#448)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperisager authored Oct 9, 2020
1 parent 021ac7f commit 6c1cc13
Show file tree
Hide file tree
Showing 15 changed files with 784 additions and 267 deletions.
1 change: 1 addition & 0 deletions packages/alfa-cascade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@siteimprove/alfa-device": "^0.5.0",
"@siteimprove/alfa-dom": "^0.5.0",
"@siteimprove/alfa-iterable": "^0.5.0",
"@siteimprove/alfa-json": "^0.5.0",
"@siteimprove/alfa-media": "^0.5.0",
"@siteimprove/alfa-option": "^0.5.0",
"@siteimprove/alfa-predicate": "^0.5.0",
Expand Down
116 changes: 73 additions & 43 deletions packages/alfa-cascade/src/cascade.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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 { Option } from "@siteimprove/alfa-option";
import { Context } from "@siteimprove/alfa-selector";

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

import { AncestorFilter } from "./ancestor-filter";
import { RuleTree } from "./rule-tree";
Expand All @@ -12,59 +15,86 @@ import { UserAgent } from "./user-agent";
/**
* @see https://drafts.csswg.org/css-cascade/
*/
export class Cascade {
public static of(entries: WeakMap<Element, RuleTree.Node>): Cascade {
return new Cascade(entries);
export class Cascade implements Serializable {
private static readonly _cascades = Cache.empty<
Document | Shadow,
Cache<Device, Cascade>
>();

public static of(node: Document | Shadow, device: Device): Cascade {
return this._cascades
.get(node, Cache.empty)
.get(device, () => new Cascade(node, device));
}

private readonly _entries: WeakMap<Element, RuleTree.Node>;
private readonly _root: Document | Shadow;
private readonly _device: Device;
private readonly _selectors: SelectorMap;
private readonly _rules = RuleTree.empty();

private constructor(entries: WeakMap<Element, RuleTree.Node>) {
this._entries = entries;
}
private readonly _entries = Cache.empty<
Element,
Cache<Context, Option<RuleTree.Node>>
>();

public get(element: Element): Option<RuleTree.Node> {
return Option.from(this._entries.get(element));
}
}
private constructor(root: Document | Shadow, device: Device) {
this._root = root;
this._device = device;
this._selectors = SelectorMap.from([UserAgent, ...root.style], device);

export namespace Cascade {
const cache = Cache.empty<Device, Cache<Document | Shadow, Cascade>>();

export function from(node: Document | Shadow, device: Device): Cascade {
return cache.get(device, Cache.empty).get(node.freeze(), () => {
const filter = AncestorFilter.empty();
const ruleTree = RuleTree.empty();
const selectorMap = SelectorMap.of([UserAgent, ...node.style], device);

return Cascade.of(
Iterable.reduce(
Iterable.flatMap(node.children(), visit),
(entries, [element, entry]) => entries.set(element, entry),
new WeakMap()
)
);
// 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 filter = AncestorFilter.empty();

const visit = (node: Node): void => {
if (Element.isElement(node)) {
this.get(node);
filter.add(node);
}

function* visit(node: Node): Iterable<[Element, RuleTree.Node]> {
if (Element.isElement(node)) {
const rules = selectorMap.get(node, filter);
for (const child of node.children()) {
visit(child);
}

const entry = ruleTree.add(sort(rules));
if (Element.isElement(node)) {
filter.remove(node);
}
};

if (entry.isSome()) {
yield [node, entry.get()];
}
visit(root);
}

filter.add(node);
public get(
element: Element,
context: Context = Context.empty()
): Option<RuleTree.Node> {
return this._entries
.get(element, Cache.empty)
.get(context, () =>
this._rules.add(sort(this._selectors.get(element, context)))
);
}

for (const child of node.children()) {
yield* visit(child);
}
public toJSON(): Cascade.JSON {
return {
root: this._root.toJSON(),
device: this._device.toJSON(),
selectors: this._selectors.toJSON(),
rules: this._rules.toJSON(),
};
}
}

filter.remove(node);
}
}
});
export namespace Cascade {
export interface JSON {
[key: string]: json.JSON;
root: Document.JSON | Shadow.JSON;
device: Device.JSON;
selectors: SelectorMap.JSON;
rules: RuleTree.JSON;
}
}

Expand Down
38 changes: 34 additions & 4 deletions packages/alfa-cascade/src/rule-tree.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Declaration, Rule } from "@siteimprove/alfa-dom";
import { Serializable } from "@siteimprove/alfa-json";
import { None, Option } from "@siteimprove/alfa-option";
import { Selector } from "@siteimprove/alfa-selector";

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

/**
* The rule tree is a data structure used for storing the rules that match each
* element when computing cascade for a document. Rules are stored in order
Expand Down Expand Up @@ -44,7 +47,7 @@ import { Selector } from "@siteimprove/alfa-selector";
*
* @see http://doc.servo.org/style/rule_tree/struct.RuleTree.html
*/
export class RuleTree {
export class RuleTree implements Serializable {
public static empty(): RuleTree {
return new RuleTree();
}
Expand Down Expand Up @@ -77,10 +80,16 @@ export class RuleTree {

return parent;
}

public toJSON(): RuleTree.JSON {
return this._children.map((node) => node.toJSON());
}
}

export namespace RuleTree {
export class Node {
export type JSON = Array<Node.JSON>;

export class Node implements Serializable {
public static of(
rule: Rule,
selector: Selector,
Expand Down Expand Up @@ -135,9 +144,9 @@ export namespace RuleTree {
rule: Rule,
selector: Selector,
declarations: Iterable<Declaration>,
children: Array<RuleTree.Node>,
children: Array<Node>,
parent: Option<Node>
): RuleTree.Node {
): Node {
if (parent.some((parent) => parent._selector === selector)) {
return parent.get();
}
Expand All @@ -160,5 +169,26 @@ export namespace RuleTree {

return node;
}

public toJSON(): Node.JSON {
return {
rule: this._rule.toJSON(),
selector: this._selector.toJSON(),
declarations: [...this._declarations].map((declaration) =>
declaration.toJSON()
),
children: this._children.map((node) => node.toJSON()),
};
}
}

export namespace Node {
export interface JSON {
[key: string]: json.JSON;
rule: Rule.JSON;
selector: Selector.JSON;
declarations: Array<Declaration.JSON>;
children: Array<Node.JSON>;
}
}
}
Loading

0 comments on commit 6c1cc13

Please sign in to comment.