What is a template engine?

The basic mechanism of a template engine is substitution (transformation), which converts specified tags into desired business data. Transforms the output of the specified pseudo statement according to some process

The Mustache template engine is familiar to VUE users

A mustache template is a string containing any number of mustache tags. Tags are indicated by the moustaches that surround them.

As officially stated, Mustache’s template syntax is very succinct, so here are a few common ones.

  • {{data}}

The double curly brace is the identifier of mustache syntax. The data in the curly brace represents the key name and is used to directly output the key value that matches the key name.

var template = `
<div>{{data}}</div>
`
var data = "hello world"
var html = mustache.render(template,data)
/ / output
`
<div>hello world</div>
`
Copy the code
  • {{...}}

{{#data}} and {{/data}} are used together.

  • {{# data}} and {{/ data}}

This syntax starts with # and ends with/for blocks that are used to render the data in the current context once or more, similar to the V-for instruction in VUE.

// The li tag iterates over the key values corresponding to the data attribute hobbies
var template = ` 
      
{{#hobbies}}
  • {{.}}
  • {{/hobbies}}
    `
    ; var hobbies = ["Sing"."Jump"."rap"] var html = mustache.render(template) / / output Sing ` < div > < li > < / li > < li > jump < / li > < li > rap < / li > < / div > ` Copy the code

    I’m very interested in implementing a toned-down version of The Mustache template engine, which includes implementations of the above syntax.

    Render function

    window.my_templateEngine = {
      // Render function
      render(templateStr, data) {
        // Convert string templates into tokens array
        var tokens = parserTemplateToTokens(templateStr);
        // Combine the data render template
        var resTemplate = renderTemplate(data, tokens)
        return resTemplate
      }
    }
    Copy the code

    The core of a template engine is its render function, which is mainly responsible for combining string templates with data to dynamically render HTML.

    Nested Arrays

    Tokens is the underlying core mechanism of Mustache: nested arrays of Js that are JS representations of template strings, and Tokens are the source of ideas for abstract syntax trees (AST), virtual nodes, and so on.

    Array form:

    
    0: (2) ['text', '\n    <ul>\n      ']
    1: (3) ['#', 'students', Array(5)]
    2: (2) ['text', '\n    </ul>\n    ']
    Copy the code

    ‘text’ represents a plain string, concatenated directly, the second item is the string ‘name’ represents the key storing the basic type, the second item is the key names ‘#’ and ‘/’ represents the block, the second item is the key name, and the third item is the array

    To convert a string template into an array of tokens, you have to do something with the string template you pass in. You know that Mustache syntax does data substitution by recognizing the parameters inside the double curly braces {{}}, so you have to use some kind of recognition method to extract the contents of {{}}. The Scanner is used to do this

    Scanner (Scanner)

    export default class Scanner {
      constructor(templateStr) {
        // Store the string template passed in
        this.templateStr = templateStr
        // Store the string before the tag
        this.tail = templateStr
        // Scan the pointer
        this.pos = 0
      }
      // The scan method, passing in the tag to be identified
      scanUntil(stopTag) {
        // Record the pos
        var pos_backup = this.pos
        // If the tail does not start with stopTag, no stopTag is detected
        while(this.tail.indexOf(stopTag) ! = =0&&!this.isEnd()) {
          this.pos ++;
          this.tail = this.templateStr.substr(this.pos)
        }
        return this.templateStr.substring(pos_backup,this.pos)
      }
      // Skip the tag passed in
      scan(tag) {
        while(this.tail.indexOf(tag) === 0) {
        / / off the tag
          this.pos += 2;
          this.tail = this.templateStr.substr(this.pos)
        }
      }
      // Determine if the end is reached
      isEnd() {
        return this.pos > this.templateStr.length
      }
    }
    Copy the code

    The scanUntil() method retrieves the string passed in, increments the pos pointer by one if the character in the current string whose index is 0 is not ‘{‘, until a ‘{‘ is encountered, which returns the string before ‘{‘. The scan() method is responsible for skipping the tag, because “{{” takes up two characters and has done its job, truncate and add 2 to the pos position. Here is an example of this method

    / / sample
    import Scanner from "./scanner";
    var scanner = new Scanner(`<div><ul><li>{{hey}}</li></ul></div>`)
    console.log(scanner.scanUntil({{" ")) //<div><ul><li>
    scanner.scan({{" ") / / to skip
    console.log(scanner.scanUntil("}}")) //hey
    scanner.scan("}}") / / to skip
    console.log(scanner.scanUntil("}}")) //</li></ul></div>.Copy the code

    Get Tokens with a scanner

    import Scanner from "./scanner";
    import nestTokens from "./nestTokens"
    export default function parserTemplateToTokens(templateStr) {
      var tokens = [];
      var words = "";
      var scanner = new Scanner(templateStr)
      while (scanner.eos()) {
        // Collect the value before mustache begins
        words = scanner.scanUntil({{" ");
        // Pass the flag
        scanner.scan({{" ")
        if(words ! = =' ') {
          tokens.push(["text", words]);
        }
        // Collect the value before the mustache ending tag
        words = scanner.scanUntil("}}");
        // Pass the flag
        scanner.scan("}}")
        if(words ! = =' ') {
          if(words[0= = ="#") {
          tokens.push(["#", words.substring(1)]);
        } else if(words[0= = ="/"){
          tokens.push(["/", words.substring(1)]); }else {
          tokens.push(["name", words]); }}}return tokens
    }
    Copy the code

    Using the sample method, you can implement an array of truncated strings that looks like this:

    0: (2) [' text ', '\ n < ul > \ n'] 1: (3) [' # ', 'students', Array (5)] 2: (2) [' text', '\ n < li > \ n students'] 3: (2) [' name ', 'name'] 4: (2) [' text ', 'hobby is \ n < ol > \ n'] 5: (3) [' # ', 'hobbies, Array (3)] 6: (2) [' text', '\ n < li >'] 7: (2) ['name', '.'] 8: (2) ['text', '</li>\n '] 9: (2) ['/', 'hobbies'] 10: (2) ['text', '\n </ol>\n </li>\n '] 11: (2) ['/', 'students'] 12: (2) ['text', '\n </ul>\n ']Copy the code

    As you can see, there are two blocks in the array: hobbies and students. We expect them to be nested (like the nested form below), so we still have some problems with the implementation.

    0: (2) [' text ', '\ n < ul > \ n'] 1: Array (3) 0:1: "#", "students" 2: Array (5) 0: (2) [' text ', '\ n < li > \ n students'] 1: (2) [' name ', 'name'] 2: (2) [' text ', 'hobby is \ n < ol > \ n'] 3: Array (3) 0:1: "#", "hobbies" 2: Array (3) 0: (2) ['text', '\n <li>'] 1: (2) ['name', '.'] 2: (2) ['text', '</li>\n '] 4: (2) ['text', '\n </ol>\n </li>\n '] 2: (2) ['text', '\n </ul>\n ']Copy the code

    An array of folding

    export default function nestTokens(tokens) {
      // Go through the number group and get #
      let nestTokens = [];
      let tagStack = [];
      for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];
        switch (token[0]) {
          case "#":
            // This is a hierarchy (several #), I can go back later
            tagStack.push(token);
            break;
          case "/":
            let tagStackPop = tagStack.pop();
            // When the first/is triggered, we can determine whether the current stack has any value, and if so, push the value to the top of the stack group
            if(tagStack.length) {
              tagStack[tagStack.length - 1] [2].push(tagStackPop)
            }else {
              nestTokens.push(tagStackPop)
            }
            break;
          default:
            if (tagStack.length === 0) {
              nestTokens.push(token)
            } else {
              tagStack[tagStack.length - 1] [2].push(token)
            }
            break; }}return nestTokens
    }
    Copy the code

    The nestTokens() method traverses the array of tokens passed in. The common tokens enter the array of Tokens. If you encounter a nested start flag #, press it into the tagStack. The subsequent push of nestTokens is in the top of the tagStack. If the nested end flag/is present, you can determine whether the current tagStack still has a value. If there is one, push it to the top of the stack. If there is none, push the last array of tokens into nestTokens and return nestTokens.

    Nested complex type reads

    If we need to get the value of the object type, we need to use. However, current rendering functions can only recognize simple data types, so we need to encapsulate a method to retrieve complex types in the data.

    // Find the value of the object based on the dotted string argument passed in
    export default function lookup(dataObj, keyName) {
      let str = ' '
      try {
        let i = keyName.indexOf('. ')
        if(i ! = = -1&& keyName ! = ='. ') {
          str = keyName.substring(0, i);
          return lookup(dataObj[str], keyName.substr(i + 1))}else {
          return dataObj[keyName]
        }
      } catch(err) {
        console.log(err); }}Copy the code

    The last step is template rendering

    Now that we have the final array of Tokens, the next step is the final but equally important one, template rendering, combining Tokens with data to form the final domStr.

    import lookup from "./lookup"
    import parserArr from "./parserArr"
    // Implement functions to combine data with tokens array into HTML
    export default function renderTemplate(data, tokens) {
      let resStr = ' ';
      for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i]
        if (token[0= = ='text') {
          resStr += token[1]}else if (token[0= = ='name') {
          resStr += lookup(data, token[1])}else if (token[0= = =The '#') {
          resStr += parserArr(token, data)
        }
      }
      return resStr
    }
    
    Copy the code

    test

     var template = ` < ul > {{# students}} {{name}} < li > the students hobby is < ol > < li > {{# hobbies}} {{...}} < / li > {{/ hobbies}} < / ol > < / li > {{/ students}} < / ul > `
        var data = {
          students: [{
              'name': 'Ming'.'hobbies': ['swimming'.The 'fitness'] {},'name': 'little red'.'hobbies': ['football'.'basketball'.'badminton'] {},'name': 'little he'.'hobbies': ['sing'.'jump'.'rap'.'basketball']},]}var domStr = window.my_templateEngine.render(template, data);
        var container = document.getElementById("container");
        container.innerHTML = domStr
    Copy the code

    Browser output