Hello, I am the smiling snail, 🐌.
In the last article, we parsed out the overall style sheet.
Today, we’ll show you how to generate a style tree.
Simply put, a style tree determines the style of each node in the DOM tree. On the basis of the style sheet, calculate the style rules that match the node and associate them with it.
A style tree is also a tree, but with style information. So how do you calculate the style of the node based on the style sheet?
Now, let’s talk about it.
The CSS style of a node can be declared in one of the following ways:
- The element
- id
- class
The rules in the stylesheet contain this information.
Therefore, the main task is: how to match the information declared by nodes with CSS rules to obtain all the rules that meet the conditions.
The data structure
For the first step, let’s still consider how to define data structures.
Since nodes need to be associated with the corresponding style, it is natural to think that the data structure needs to contain both node information and style information, which is stored in a map.
In addition, the style tree is also a tree structure, containing child nodes.
/ / map
typealias StyleMap = [String: Value]
struct StyleNode {
/ / the node
var node: Node
// The associated style
var styleMap: StyleMap
/ / child nodes
var children: [StyleNode]
}
Copy the code
Because a node may declare more than one rule, the same attribute declaration may exist in different rules.
Because the selector in the rule has a priority. When matching, it is natural to choose a higher priority.
For example, in the following example, two rules set width at the same time, but depending on the priority, the last value used should be the property in.test.
div {
width: 100px;
}
.test {
width: 200px;
}
<div class="test"></div>
Copy the code
Finally, all attributes that match the style are put into the map. Depending on the nature of the map, insert the same key, and the value of the latter overrides the former.
For the same attribute name, we need to ensure that the higher-priority attribute overrides the lower-priority attribute.
In this way, higher-priority attributes are required to be put into the map later than lower-priority attributes.
Therefore, after obtaining all matching rules, you need to sort the rules in descending order of priority to ensure that the highest rule has the lowest priority.
To do this, define the following structure that associates priorities with rules for auxiliary sorting.
// Specifity is also used for sorting
typealias MatchedRule = (Specifity, Rule)
// Specifity is a triad defined in the previous article
// It is used to sort the selectors. The priority is id, class, tag
typealias Specifity = (Int, Int, Int)
Copy the code
Node style matching
There can be multiple rules that match a node in a stylesheet. So, the process of styling a node requires traversing the entire stylesheet.
A single rule matches
First, let’s look at matching a single rule.
From the previous article, we know that CSS rules = selector list + property list.
As long as it matches one of the selectors in the list of selectors, this rule is valid. Therefore, the focus shifts to selector matching.
1. Selector matching
Selector information includes tag, ID, and classes, and the same information can be obtained from node data. For example, id and class can be obtained from attributes, not to mention tag.
So that’s a good way to match. Note, however, that tag, ID, and classes in the selector do not necessarily have data.
The matching rules are as follows:
- If there is a tag in the selector, check whether the tag is the same as the node’s tag. If they are different, they are not matched.
- If there is an ID in the selector, it is checked to see if the ID is the same as the node ID. If they are different, they are not matched.
- If there is a class in the selector, the class list is checked to see if it is fully contained by the class attribute declared by the node. If it is not, it indicates a mismatch.
- In other cases, it is a match.
Notice the third class match. Fully inclusive, that is, the class in the selector must be a subset of the class declared by the node.
div.test1.test2 {}
// Fully included
<div class="test1 test2 test3"></div>
Copy the code
The matching code for a single selector is as follows:
// Whether the node id, tag, and class match the simpleSelector selector. If one does not match, return false
func matchSelector(node: ElementData, simpleSelector: SimpleSelector) -> Bool {
// tag, the CSS has tags and is not equal
ifsimpleSelector.tagName ! =nil&& node.tagName ! = simpleSelector.tagName {return false
}
// id
let id = node.getId()
// The CSS has ids that are not equal
ifsimpleSelector.id ! =nil && id! = simpleSelector.id {return false
}
// class
let classes = node.getClasses()
let selectorClasses = simpleSelector.classes
// The class of the node element contains all classes of selector
for cls in selectorClasses {
if! classes.contains(cls) {return false}}return true
}
Copy the code
This completes the matching of the individual selectors.
2. The selector list matches
Matching the list of selectors follows naturally, looping through the list to see if it matches one by one.
When a rule is matched, it returns. Because in the previous article on CSS parsing, the list of selectors was already sorted from highest to lowest, you only need to match to.
Finally returns a tuple of priorities and rules for sorting.
func matchRule(node: ElementData, rule: Rule) -> MatchedRule? {
// Iterate through the selectors for the rule
for selector in rule.selectors {
if case .Simple(let simpleSelector) = selector {
// If it matches
if matchSelector(node: node, simpleSelector: simpleSelector) {
return (selector.specificity(), rule)
}
}
}
return nil
}
Copy the code
Multiple rule matching
Now that a single rule has been matched, multiple rules are easy.
Iterate through the stylesheet to see if it matches, and return matching rules.
// Walk through the stylesheet to find matching rules
func matchRules(node: ElementData, styleSheet: StyleSheet) -> [MatchedRule] {
let rules = styleSheet.rules.compactMap { (rule) -> MatchedRule? in
let result = matchRule(node: node, rule: rule)
return result
}
return rules
}
Copy the code
Generate style map
Now that you have a list of matched rules, this step requires putting all the attributes of the rules into the map.
But wait, remember that priority question? High priority must be added later.
Therefore, you must first sort the list of rules from lowest to highest in order to ensure that the final attribute values are correct.
The code is simple, as follows:
// Generate a style map
func genStyleMap(node: ElementData, styleSheet: StyleSheet) -> StyleMap {
var styleMap = StyleMap()
// Get the matching rule
var rules = matchRules(node: node, styleSheet: styleSheet)
// Sort from low priority to high priority, so that the higher priority overrides the lower priority when put into the map
rules.sort {
$0.0The < $1.0
}
// Iterate over all attribute declarations matching the rule
for (_, rule) in rules {
let declarations = rule.declarations
for declaration in declarations {
// Add the map one by one
styleMap[declaration.name] = declaration.value
}
}
return styleMap
}
Copy the code
Generate style tree
The final step is to generate the final product, the style tree. Now that you can get the style of individual nodes, for tree structures, recursive traversal can get the style of the entire tree.
Note, however, that only elements have styles, not text nodes, so generate an empty map for them.
// Generate a style tree
func genStyleTree(root: Node, styleSheet: StyleSheet) -> StyleNode {
var styleMap: StyleMap
let nodeType = root.nodeType
switch nodeType {
// The text node has no style
case .Text(_):
styleMap = [:]
case .Element(let node):
// Generate a style map
styleMap = genStyleMap(node: node, styleSheet: styleSheet)
}
// The child nodes recursively generate the association style
let childrenStyleNodes = root.children.map { (child) -> StyleNode in
genStyleTree(root: child, styleSheet: styleSheet)
}
return StyleNode(node: root, styleMap: styleMap, children: childrenStyleNodes)
}
Copy the code
The test code
// Style association processing
let styleProcessor = StyleProcessor()
let styleNode = styleProcessor.genStyleTree(root: root, styleSheet: styleSheet)
print(styleNode)
Copy the code
Taking the results of HTML parsing and CSS parsing as input, you get a style tree.
The full code can be viewed at: github.com/silan-liu/t… .
The last
If you think the article is helpful, you can click on the following business card to pay attention to the public number “smiling snail”.
Reply “snail” in the chat box of the public number, you can add wechat for communication ~