Let's build browser engine! in typescript vol9 DOM and Rules to Style tree
Murahashi [Matt] Kenichi
Posted on May 26, 2019
Transform DOM node and Rules to StyleTree!
test("matchRule none-match", () => {
expect(matchRule(new ElementData("no mean", new Map([])), new Rule([], []))).toBeNull();
});
test("matchRule match first", () => {
const rule = new Rule(
[
// specificity a=1, b=0, c=0
new Selector.Simple(new SimpleSelector(null, "target", [])),
// specificity a=0, b=0, c=1
new Selector.Simple(new SimpleSelector("target", null, []))
],
[]
);
expect(matchRule(new ElementData("target", new Map([["id", "target"]])), rule)).toEqual([
[1, 0, 0],
rule
]);
});
export type MatchedRule = [Specificity, Rule];
// If `rule` matches `elem`, return a `MatchedRule`. Otherwise return null.
export function matchRule(elem: ElementData, rule: Rule): null | MatchedRule {
// Find the first (most specific) matching selector.
// Because our CSS parser stores the selectors from most- to least-specific,
const found = rule.selectors.find(selector => {
return matches(elem, selector);
});
if (found === undefined) {
return null;
}
return [found.selector.specificity(), rule];
}
test("matchingRules none-match", () => {
expect(matchingRules(new ElementData("no mean", new Map([])), new Stylesheet([]))).toEqual([]);
});
test("matchingRules matches", () => {
const rule1 = new Rule(
[
// specificity a=1, b=0, c=0
new Selector.Simple(new SimpleSelector(null, "target", [])),
// specificity a=0, b=0, c=1
new Selector.Simple(new SimpleSelector("target", null, []))
],
[]
);
const rule2 = new Rule(
[
// specificity a=0, b=0, c=1
new Selector.Simple(new SimpleSelector("target", null, []))
],
[]
);
expect(
matchingRules(
new ElementData("target", new Map([["id", "target"]])),
new Stylesheet([rule1, rule2])
)
).toEqual([[[1, 0, 0], rule1], [[0, 0, 1], rule2]]);
});
export function matchingRules(elem: ElementData, stylesheet: Stylesheet): MatchedRule[] {
return stylesheet.rules
.map(rule => {
return matchRule(elem, rule);
})
.filter(
(matchedOrNull): matchedOrNull is MatchedRule => {
return matchedOrNull !== null;
}
);
}
test("specifiedValues none", () => {
expect(specifiedValues(new ElementData("no mean", new Map([])), new Stylesheet([]))).toEqual(
new Map([])
);
});
test("specifiedValues", () => {
const rule1 = new Rule(
[
// specificity a=1, b=0, c=0
new Selector.Simple(new SimpleSelector(null, "target", [])),
// specificity a=0, b=0, c=1
new Selector.Simple(new SimpleSelector("target", null, []))
],
[
new Declaration("override", new CssValue.Keyword("current")),
new Declaration("not-override1", new CssValue.Keyword("value1"))
]
);
const rule2 = new Rule(
[
// specificity a=0, b=0, c=1
new Selector.Simple(new SimpleSelector("target", null, []))
],
[
new Declaration("override", new CssValue.Keyword("prev")),
new Declaration("not-override2", new CssValue.Keyword("value2"))
]
);
expect(
specifiedValues(
new ElementData("target", new Map([["id", "target"]])),
new Stylesheet([rule1, rule2])
)
).toEqual(
new Map([
["not-override1", new CssValue.Keyword("value1")],
["not-override2", new CssValue.Keyword("value2")],
["override", new CssValue.Keyword("current")]
])
);
});
test("compare matched rule right a", () => {
expect(
compareMatchedRule([[0, 0, 0], new Rule([], [])], [[1, 0, 0], new Rule([], [])])
).toBeLessThan(0);
});
test("compare matched rule left a", () => {
expect(
compareMatchedRule([[1, 0, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
).toBeGreaterThan(0);
});
test("compare matched rule right b", () => {
expect(
compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 1, 0], new Rule([], [])])
).toBeLessThan(0);
});
test("compare matched rule left b", () => {
expect(
compareMatchedRule([[0, 1, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
).toBeGreaterThan(0);
});
test("compare matched rule right c", () => {
expect(
compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 0, 1], new Rule([], [])])
).toBeLessThan(0);
});
test("compare matched rule left c", () => {
expect(
compareMatchedRule([[0, 0, 1], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
).toBeGreaterThan(0);
});
test("compare matched rule same", () => {
expect(compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])).toBe(0);
});
test("sort compare matched rule", () => {
const left: MatchedRule = [[0, 0, 0], new Rule([], [])];
const right: MatchedRule = [[1, 0, 0], new Rule([], [])];
expect([left, right].sort(compareMatchedRule)).toEqual([left, right]);
});
export function compareMatchedRule(left: MatchedRule, right: MatchedRule): number {
const [[la, lb, lc]] = left;
const [[ra, rb, rc]] = right;
if (la !== ra) {
return la - ra;
} else if (lb !== rb) {
return lb - rb;
} else if (lc !== rc) {
return lc - rc;
}
return 0;
}
// Apply styles to a single element, returning the specified styles.
//
// To do: Allow multiple UA/author/user stylesheets, and implement the cascade.
export function specifiedValues(elem: ElementData, stylesheet: Stylesheet): PropertyMap {
const values = new Map<string, CssValue>([]);
const rules = matchingRules(elem, stylesheet);
rules.sort(compareMatchedRule);
for (const [, rule] of rules) {
for (const declaration of rule.declarations) {
values.set(declaration.name, declaration.value);
}
}
return values;
}
test("style node text", () => {
expect(styleTree(text("hoge"), new Stylesheet([]))).toEqual(
new StyledNode(text("hoge"), new Map([]), [])
);
});
test("style node element", () => {
const rule = new Rule(
[new Selector.Simple(new SimpleSelector(null, "target", []))],
[new Declaration("some", new CssValue.Keyword("foo"))]
);
const element = elem("no mean", new Map([["id", "target"]]), []);
expect(styleTree(element, new Stylesheet([rule]))).toEqual(
new StyledNode(element, new Map([["some", new CssValue.Keyword("foo")]]), [])
);
});
test("style node children", () => {
const rule = new Rule(
[new Selector.Simple(new SimpleSelector(null, "target", []))],
[new Declaration("some", new CssValue.Keyword("foo"))]
);
const element = elem("no mean", new Map([]), [elem("no mean", new Map([["id", "target"]]), [])]);
expect(styleTree(element, new Stylesheet([rule]))).toEqual(
new StyledNode(element, new Map([]), [
new StyledNode(
elem("no mean", new Map([["id", "target"]]), []),
new Map([["some", new CssValue.Keyword("foo")]]),
[]
)
])
);
});
// Apply a stylesheet to an entire DOM tree, returning a StyledNode tree.
export function styleTree(root: DomNode, stylesheet: Stylesheet): StyledNode {
switch (root.nodeType.format) {
case NodeType.Format.Text:
return new StyledNode(
root,
new Map([]),
// NOTE: text node has children??? I'm not sure
[]
);
case NodeType.Format.Element:
return new StyledNode(
root,
specifiedValues(root.nodeType.element, stylesheet),
root.children.map(child => {
return styleTree(child, stylesheet);
})
);
}
}
references
- Let's build a browser engine! Part 1: Getting started
- mbrubeck/robinson
- sanemat/js-toy-engine
- sanemat/ts-toy-engine
series
- Let's build browser engine! in typescript vol0 Toy browser engine
- Let's build browser engine! in typescript vol1 Canvas with Color
- Let's build browser engine! in typescript vol2 Display Command
- Let's build browser engine! in typescript vol3 Layout Box, Dimensions
- Let's build browser engine! in typescript vol4 Layout Tree to Display List
- Let's build browser engine! in typescript vol5 DOM Shortcut
- Let's build browser engine! in typescript vol6 CSS Shortcut
- Let's build browser engine! in typescript vol7 Selector matching
- Let's build browser engine! in typescript vol8 Specificity
- Let's build browser engine! in typescript vol9 DOM and Rules to Style tree
💖 💪 🙅 🚩
Murahashi [Matt] Kenichi
Posted on May 26, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
webdev Understanding HTTP, Cookies, Email Protocols, and DNS: A Guide to Key Internet Technologies
November 30, 2024
react Axios NPM Package: A Beginner's Guide to Installing and Making HTTP Requests
November 30, 2024
webdev Guide to Cookies, Local Storage, Session Storage, and Other Web Storage Mechanisms
November 30, 2024