Simple implementation of ejs template engine

1. Causes

In a recent sharing by the Department, someone proposed to implement an ejs template engine. Suddenly, he found that he had never considered this problem before and had always taken it directly for use. Let's do it. This paper mainly introduces the simple use of ejs, not all of which are implemented. The part related to options configuration is directly omitted. If there is any mistake, please point it out. Finally, you are welcome to like + collect.

2. Basic syntax implementation

Define the render function to receive html strings and data parameters.

const render = (ejs = '', data = {}) => {

}

The case template string is as follows:

<body>
    <div><%= name %></div>
    <div><%= age %></div>
</body>

You can use regular to match <% = name% > and keep only name. Here, the template string of ES6 is used. Wrap name in ${}.

The second value in props is the matching variable. Direct props[1] replacement.

[
  '<%= name %>',
  ' name ',
  16,
  '<body>\n    <div><%= name %></div>\n    <div><%= age %></div>\n</body>'
]
const render = (ejs = '', data = {}) => {
    const html = ejs.replace(/<%=(.*?)%>/g, (...props) => {
        return '${' + props[1] + '}';
        // return data[props[1].trim()];
    });
}

3. Function function

The html obtained here is a template string. You can program strings into executable functions through Function. Of course, eval can also be used here. It's up to you.

<body>
    <div>${ name }</div>
    <div>${ age }</div>
</body>

Function is a constructor that returns a real function after instantiation. The last parameter of the constructor is the string of the function body, and the previous parameters are formal parameters. For example, the formal parameter name is passed here, and the function body is passed through console Log print a sentence.

const func = new Function('name', 'console.log("I passed Function Built function, my name is:" + name)');
// Execute function, pass in parameters
func('yindong'); // I build a Function through Function. My name is: yindong

Using the ability of Function, you can execute and return html template strings. Write a return Function string to return an assembled template string

const getHtml = (html, data) => {
    const func = new Function('data', `return \`${html}\`;`);
    return func(data);
    // return eval(`((data) => {  return \`${html}\`; })(data)`)
}

const render = (ejs = '', data = {}) => {
    const html = ejs.replace(/<%=(.*?)%>/g, (...props) => {
        return '${' + props[1] + '}';
    });
    return getHtml(html, data);
}

4 with

Here, props[1] in the render function is actually the variable name, that is, name and age, which can be replaced by data [props[1] Trim ()], but there will be some problems with this writing. Steal a lazy use of the feature of the with code block.

The with statement is used to extend the scope chain of a statement. In other words, the variables used in the with statement will be looked for in the with statement first. If they are not found, they will be looked up.

For example, an age number and a data object are defined here, and the data contains a name string. The name output in the code block of the with package will be searched in the data first. If the age does not exist in the data, it will be searched upward. Of course, this feature is also a reason why with is not recommended, because it is uncertain whether the variables in the with statement are in data.

const age = 18;
const data = {
    name: 'yindong'
}

with(data) {
    console.log(name);
    console.log(age);
}

Here, use with to modify the getHtml function. The function body is wrapped with with with, and data is the passed in parameter data. In this way, all used variables in the with body are found from data.

const getHtml = (html, data) => {
    const func = new Function('data', `with(data) { return \`${html}\`; }`);
    return func(data);
    // return eval(`((data) => { with(data) { return \`${html}\`; } })(data)`)
}

const render = (ejs = '', data = {}) => {
    // Optimize the code and directly replace props[1] with $1;
    // const html = ejs.replace(/<%=(.*?)%>/g, (...props) => {
    //     return '${' + props[1] + '}';
    // });
    const html = ejs.replace(/<%=(.*?)%>/gi, '${$1}');
    return getHtml(html, data);
}

So you can print out real html.

<body>
    <div>yindong</div>
    <div>18</div>
</body>

5. ejs statement

Here, extend ejs and add an arr.join statement.

<body>
    <div><%= name %></div>
    <div><%= age %></div>
    <div><%= arr.join('--') %></div>
</body>
const data = {
    name: "yindong",
    age: 18,
    arr: [1, 2, 3, 4]
}

const html = fs.readFileSync('./html.ejs', 'utf-8');

const getHtml = (html, data) => {
    const func = new Function('data', ` with(data) { return \`${html}\`; }`);
    return func(data);
}

const render = (ejs = '', data = {}) => {
    const html = html = ejs.replace(/<%=(.*?)%>/gi, '${$1}');
    return getHtml(html, data);
}

const result = render(html, data);

console.log(result);

It can be found that ejs can also be compiled normally. Because the template string supports arr.join syntax, the output is:

<body>
    <div>yindong</div>
    <div>18</div>
    <div>1--2--3--4</div>
</body>

If ejs contains a forEach statement, it is more complex. At this point, the render function cannot be parsed normally.

<body>
    <div><%= name %></div>
    <div><%= age %></div>
    <% arr.forEach((item) => {%>
        <div><%= item %></div>
    <%})%>
</body>

There are two steps to deal with it. Careful observation shows that there is a = sign in the way of using variable value, while there is no = sign in the statement. You can perform the first step of processing the ejs string by replacing the <% = variable with the corresponding variable, that is, the original render function code remains unchanged.

const render = (ejs = '', data = {}) => {
    const html = ejs.replace(/<%=(.*?)%>/gi, '${$1}');
    console.log(html);
}
<body>
    <div>${ name }</div>
    <div>${ age }</div>
    <% arr.forEach((item) => {%>
        <div>${ item }</div>
    <%})%>
</body>

The second step is to wrap around a little. The above string can be processed into multiple string splicing. For a simple example, convert the result of a plus arr.forEach plus c into str, store a, splice each result of arr.forEach, and splice c. This will get the correct string.

// Original string
retrun `
    a
    <% arr.forEach((item) => {%>
        item
    <%})%>
    c
`
// Spliced
let str;
str = `a`;

arr.forEach((item) => {
    str += item;
});

str += c;

return str;

Use / <% (. *?)% > on the result of the first step/ G regular matching out the content in the middle of <%% >, that is, the second step.

const render = (ejs = '', data = {}) => {
    // First step
    let html = ejs.replace(/<%=(.*?)%>/gi, '${$1}');
    // Step 2
    html = html.replace(/<%(.*?)%>/g, (...props) => {
        return '`\r\n' + props[1] + '\r\n str += `';
    });
    console.log(html);
}

The replaced string looks like this.

<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})
 str += `
</body>

Adding a line change will make it easier to see. It can be found that the first part is the string missing the header \ ', and the second part is the complete js part of the forEach loop content stored in str, which is executable. The third part is the string missing the tail \ '.

// Part I
<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `

// Part II
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})

// Part III
 str += `
</body>

Process and complete the string. Add let str = \ ` in the first part, so that it is a complete string. The second part does not need to be processed. The execution results of the second part will be spliced based on the first part, and the third part needs to be spliced \ ` at the end; return str; That is, complete the template string at the end, and return the str complete string through return.

// Part I
let str = `<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `

// Part II
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})

// Part III
 str += `
</body>
`;

return str;

This part of logic can be added to the getHtml function. First, STR is defined in with to store the first part of the string, and the tail returns the str string through return.

const getHtml = (html, data) => {
    const func = new Function('data', ` with(data) { let str = \`${html}\`; return str; }`);
    return func(data);
}

In this way, you can execute ejs statements.

const data = {
    name: "yindong",
    age: 18,
    arr: [1, 2, 3, 4],
    html: '<div>html</div>',
    escape: '<div>escape</div>'
}

const html = fs.readFileSync('./html.ejs', 'utf-8');

const getHtml = (html, data) => {
    const func = new Function('data', ` with(data) { var str = \`${html}\`; return str; }`);
    return func(data);
}

const render = (ejs = '', data = {}) => {
    // Replace all variables
    let html = ejs.replace(/<%=(.*?)%>/gi, '${$1}');
    // Splice string
    html = html.replace(/<%(.*?)%>/g, (...props) => {
        return '`\r\n' + props[1] + '\r\n str += `';
    });
    return getHtml(html, data);
}

const result = render(html, data);

console.log(result);

Output results

<body>
    <div>yindong</div>
    <div>18</div>

        <div>1</div>

        <div>2</div>

        <div>3</div>

        <div>4</div>

</body>

6. Label escape

<% = the incoming html will be escaped. Write an escape html escape function here.

const escapeHTML = (str) => {
    if (typeof str === 'string') {
        return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/ /g, "&nbsp;").replace(/"/g, "&#34;").replace(/'/g, "&#39;");
    } else {
        return str;
    }
}

When replacing variables, use the escape HTML function to process variables. Here, remove the spaces by \ s *. In order to avoid naming conflicts, escapeHTML is transformed into a self executing function with a function parameter of $1 variable name.

const render = (ejs = '', data = {}) => {
    // Replace transfer variable
    // let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, '${escapeHTML($1)}');
    let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, `\${
        ((str) => {
            if (typeof str === 'string') {
                return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/ /g, "&nbsp;").replace(/"/g, "&#34;").replace(/'/g, "&#39;");
            } else {
                return str;
            }
        })($1)
    }`);
    // Splice string
    html = html.replace(/<%(.*?)%>/g, (...props) => {
        return '`\r\n' + props[1] + '\r\n str += `';
    });
    return getHtml(html, data);
}

The getHtml function remains unchanged.

const getHtml = (html, data) => {
    const func = new Function('data', `with(data) { var str = \`${html}\`; return str; }`);
    return func(data);
}

<% - the output in the original format will be retained. You only need to add one that is not processed by the escape HTML function.

const render = (ejs = '', data = {}) => {
    // Replace escape variable
    let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, '${escapeHTML($1)}');
    // Replace the remaining variables
    html = html.replace(/<%-(.*?)%>/gi, '${$1}');
    // Splice string
    html = html.replace(/<%(.*?)%>/g, (...props) => {
        return '`\r\n' + props[1] + '\r\n str += `';
    });
    return getHtml(html, data, escapeHTML);
}

Output style

<body>
    <div>yindong</div>
    <div>18</div>

        <div>1</div>

        <div>2</div>

        <div>3</div>

        <div>4</div>

    <div>&lt;div&gt;escapeHTML&lt;/div&gt;</div>
</body>

So far, a simple ejs template interpreter has been written.

Keywords: Javascript Front-end html

Added by RiBlanced on Wed, 22 Dec 2021 22:16:10 +0200