This is the 15th day of my participation in Gwen Challenge

One, foreword

In the last part, I mainly introduced the part of generating AST syntax tree – template parsing

The HTML template is parsed and processed using re to match tags and attributes in the template

This article, generate AST syntax tree – construct tree structure


Second, build a tree structure

1. What needs to be described

As mentioned earlier, the HTML template is constructed as an AST syntax tree, which uses the JS tree structure to describe the HTML syntax

For an HTML template, the following points need to be described and documented:

  • The label
  • attribute
  • The text
  • The hierarchy of HTML structures, that is, parent-child relationships

2. How to build a father-son relationship

A simple demo:

<div><sapn></span><p></p></div>
Copy the code

Html-based tags are in pairs. Start tags and end tags are in a group, for example, and

Based on this feature, the stack data structure can be used to build parent-child relationship

[div,span,span] span is the son of div,span is the son of div,span is the son of div, Make P the son of div (p and SPAN are brothers of the same rank)... slightlyCopy the code

3. Data structure of ast node elements

// Build a parent-child relationship
function createASTElement(tag, attrs, parent) {
  return {
    tag,  / / tag name
    type:1./ / element
    children: []./ / son
    parent, / / father
    attrs / / property}}Copy the code

4. Process the start tag

Handles the logic of the start tag

  • Take the last label in the current stack as the parent node and create an AST node.
  • If it is the first node, it automatically becomes the root of the whole tree

Code implementation

// Handle the start tag, such as [div,p]
function start(tag, attrs) {
  // Take the last label on the stack as the parent node
  let parent = stack[stack.length-1];
  // Create the current AST node
  let element = createASTElement(tag, attrs, parent);
  // The first node is the root node
  if(root == null) root = element;
  // If there is a father, the parent is identified (set the father for the current node; Also set son for father)
  if(parent){
    element.parent = parent;
    parent.children.push(element)
  }
  stack.push(element)
}
Copy the code

5. Handle the closing tag

Handles the logic of the closing tag

  • Throws the last tag on the stack (that is, the start tag paired with the current end tag)
  • Verifies that the tag thrown on the stack is paired with the current closing tag

Code implementation

// Process the closing tag
function end(tagName) {
  console.log("Launch matching end tag -end,tagName =" + tagName)
  // Throw an end tag from the stack
  let endTag = stack.pop();
  // check: Whether the thrown end tag name is consistent with the current end tag name
  // Start/end tags are paired, and an error is reported when the element name thrown does not match the current element name
  if(endTag.tag ! = tagName)console.log("Wrong label")}Copy the code

6. Work with text

Logic for processing text

  • Take the last label in the current stack as the parent node
  • Removes whitespace characters that may exist in the text
  • Binds the text directly to the child of the parent node without pushing it

Code implementation

// Process text (text may contain whitespace characters)
function text(chars) {
  console.log("Launch matched text-text,chars =" + chars)
  // Find the parent of the text: the parent of the text, which is the last element in the current stack
  let parent = stack[stack.length-1];
  // Remove possible whitespace characters from whitespace text: replace whitespace with empty
  chars = chars.replace(/\s/g.""); 
  if(chars){
    // Bind the parent-child relationship
    parent.children.push({
      type:2.// type=2 indicates the text type
      text:chars,
    })
  }
}
Copy the code

Third, code refactoring

1. Modularity

Encapsulate the parserHTML logic and extract it into a separate JS file:

src/compile/parser.js#parserHTML

// src/compile/parser.js


// Match the label name: AA-xxx
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// Namespace tag :aa: aA-xxx
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `;
// Match tag name (index 1) : 
const startTagOpen = new RegExp(` ^ <${qnameCapture}`);

const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `);
Aaa =" XXX ", aaa=' XXX ', aaa= XXX
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /;
// Match the end tag: > or />
const startTagClose = /^\s*(\/?) >/;
// Match {{XXX}} to XXX
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g

export function parserHTML(html) {
  console.log("***** Go to parserHTML: Compile templates into AST syntax tree *****")
  let stack = [];
  let root = null;
  // Build a parent-child relationship
  function createASTElement(tag, attrs, parent) {
    return {
      tag,  / / tag name
      type:1.// The element type is 1
      children: []./ / son
      parent, / / father
      attrs / / property}}// Start tag, such as [div,p]
  function start(tag, attrs) {
    console.log("Launch matched start tag -start,tag =" + tag + ",attrs = " + JSON.stringify(attrs))
    // When the start tag is encountered, the last node on the stack is taken as the parent node
    let parent = stack[stack.length-1];
    let element = createASTElement(tag, attrs, parent);
    // As the root node when there is no root node
    if(root == null) root = element;
    if(parent){ // The parent node exists
      element.parent = parent;  // Sets the parent node for the current node
      parent.children.push(element) // The current node is also called the child node of the parent node
    }
    stack.push(element)
  }
  // End tag
  function end(tagName) {
    console.log("Launch matching end tag -end,tagName =" + tagName)
    // If it is an end tag, it is thrown from the stack
    let endTag = stack.pop();
    // check: Whether the thrown end tag name is consistent with the current end tag name
    if(endTag.tag ! = tagName)console.log("Wrong label")}/ / text
  function text(chars) {
    console.log("Launch matched text-text,chars =" + chars)
    Note: The text may have whitespace characters
    let parent = stack[stack.length-1];
    chars = chars.replace(/\s/g.""); // Replace Spaces with empty Spaces, that is, delete Spaces
    if(chars){
      parent.children.push({
        type:2.// The text type is 2
        text:chars,
      })
    }
  }
  /** * intercepts the string *@param {*} Len intercepts the length */
  function advance(len) {
    html = html.substring(len);
    console.log("HTML after intercepting matching content :" + html)
    console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")}/** * matches the start tag and returns the matching result */
  function parseStartTag() {
    console.log("***** go to parseStartTag and try to parse the start tag, current HTML:" + html + "* * * * *")
    // Matches the start tag, whose name is index 1
    const start = html.match(startTagOpen);
    if(start){// Match to the start tag
      // Construct the matching result, including: tag name + attribute
      const match = {
        tagName: start[1].attrs: []}console.log("HTML. Match (startTagOpen) result:" + JSON.stringify(match))
      // Intercepts the matched result
      advance(start[0].length)
      let end;  // Matches the end symbol of the start tag > or />
      let attr; // Stores the result of the attribute match
      // Matches attributes that cannot start with an end tag, such as 
      
, which is finished by > and does not continue to match attributes within that tag
// attr = html.match(attribute) Matches the attribute and assigns the matching result of the current attribute / /! (end = html.match(startTagClose)) does not match the startTagClose symbol > or /> while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) {// Push the matched attribute into the attrs array until the close symbol > is matched console.log("Match to attribute attr =" + JSON.stringify(attr)) / / the console. The log (" matching to the attribute name = "+ attr [1] +" value = "+ attr [3] | | attr [4] | | attr [5]) match.attrs.push({ name: attr[1].value: attr[3] || attr[4] || attr[5] }) advance(attr[0].length)// Intercepts the matched attribute XXX = XXX } // Match to close symbol >, while the current tag processing completes, // At this point,
if (end) { console.log(HTML. Match (startTagClose):" + JSON.stringify(end)) advance(end[0].length) } // after the start tag processing is complete, the matching result is returned: tagName tagName + attrs attribute console.log(">>>>> Match result of start tag startTagMatch =" + JSON.stringify(match)) return match; } console.log("Failed to match start tag, return false") console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =") return false; } // Continue to intercept the template until all parses are completed while (html) { // Parse tags and text (see if it starts with <) let index = html.indexOf('<'); if (index == 0) {/ / label console.log("Parse HTML:" + html + ", result: is the tag") // If it is a tag, continue parsing the start tag and attributes const startTagMatch = parseStartTag();// Match the start tag and return the matching result if (startTagMatch) { // If the tag is matched, it is the start tag // Match the start tag, call the start method, pass the tag name and attributes start(startTagMatch.tagName, startTagMatch.attrs) continue; // If it is the start tag, there is no need to continue down, continue parsing the rest of the while } let endTagMatch; if (endTagMatch = html.match(endTag)) {// This is the end tag // Match the start tag, call the start method, pass the tag name and attributes end(endTagMatch[1]) advance(endTagMatch[0].length) continue; // If it is a closing tag, there is no need to continue down, continue parsing the rest of the while}}else {/ / text console.log("Parse HTML:" + html + ", result: is text")}// Text: index > 0 if(index > 0) {// Take the text out and send it out, then remove it from the HTML let chars = html.substring(0,index) // hello</div> text(chars); advance(chars.length) } } console.log("Current template template, all parsed") return root; } Copy the code

2. Import modules

SRC /compile/index.js, importing parserHTML:

// src/compile/index.js

import { parserHTML } from "./parser";

export function compileToFunction(template) {
  console.log(***** go to compileToFunction: compile template to render function *****)
  // 1, turn the template into an AST syntax tree
  let ast = parserHTML(template);
  console.log("Parsing HTML returns ast syntax tree ====>")
  console.log(ast)
}
Copy the code

3. Test the AST build logic

<div><p>{{message}}<sapn>HelloVue</span></p></div>
Copy the code

Print the execution result, as shown in figure:

div
    p
        {{message}}
        span
            HelloVue
Copy the code

Four, the end

This article, generate AST syntax tree – construct the tree structure section

  • Based on THE characteristics of HTML, the stack data structure is used to record the parent-child relationship
  • Start tag, end tag, and text processing
  • Code reconstruction and ast syntax tree construction process analysis

Next, the AST syntax tree generates the render function