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
Top comments (0)