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 ~