I’ve recently learned mustache from the video, writing it down so you can pick it up later
preface
Mustache is a front-end template parsing engine that, for example, can parse
{{name}}
to
triple
, which is very similar to vue’s template syntax. So how does it work?
I started with regular expressions, such as the following code to implement a simple substitution
let str = ' I am {{name}}, this year {{age}}
';
let data = {
name: "Zhang".age: 18};// Use non-greedy matching
str = str.replace(/ {{(. +?) }}/g.(_, key) = > {
console.log(key, "key");
return data[key];
});
console.log(str, "str");
Copy the code
Replace the result
This does not apply to complex cases, such as arrays and nested arrays, where the re can no longer be handled. To handle more complex situations, start learning Mustache’s parsing process
Mustache parsing process
1. Parse the template string into a multidimensional array
Need to put<p> I am {{name}}, this year {{age}} </p>
This template string resolves to this multidimensional array.
First, we define a ScanUtil class that scans and parses the string one by one, storing the text before {{as an array of [text, text] and the text between {{}} as an array of [name, text]. The code is as follows:
export class ScanUtil {
// Initializes the template string and pointer position
constructor(templateStr) {
this.templateStr = templateStr;
this.pos = 0;
}
// The string after the pointer, indicating that it has not been traversed
get tail() {
return this.templateStr.substring(this.pos);
}
// Whether the traversal has been completed
get eos() {
return this.pos >= this.templateStr.length;
}
// Whether the tag string is in the beginning bit
tagIsInBegin(tag) {
return this.tail.startsWith(tag);
}
// Skip the tag position
skip(tag) {
if (this.tagIsInBegin(tag)) {
this.pos += tag.length; }}// Scan and intercept the target text
scanText(tag) {
const prePos = this.pos;
while (!this.tagIsInBegin(tag) && !this.eos) {
this.pos++;
}
let res = this.templateStr.substring(prePos, this.pos);
// Pointer skips tag
this.skip(tag);
return res;
}
// Loop through, return tokens array
compileStrToTokens() {
let res = [];
while (!this.eos) {
// intercepts the text before {{
let text = this.scanText({{" ");
// Add the result to the array
res.push(["text", text]);
// Cut the text from {{to}}
let name = this.scanText("}}");
// Add the result to the array
res.push(["name", name]);
}
returnres; }}Copy the code
Use templateStr to store incoming strings, pos to store pointer positions, scanText to scan and intercept target text, and skip to drop {{and}} positions. Now that you’re ready to handle simple cases, call this class to test:
let str = ' I am {{name}}, this year {{age}}
';
let data = {
name: "Zhang".age: 18};let scanner = new ScanUtil(str);
console.log(scanner.compileStrToTokens(),"scanner.compileStrToTokens()")
Copy the code
The following output is displayed:
But now if you need to deal with arrays, you’re two steps away.
-
Store arrays as [‘#’,’arr’] and [‘/’,’arr’] structures
# indicates the beginning of the array, and/indicates the end of the array. Modify compileStrToTokens to determine if the name value contains # or/using the re
// Loop through, return tokens array compileStrToTokens() { let res = []; while (!this.eos) { // intercepts the text before {{ let text = this.scanText({{" "); // Add the result to the array res.push(["text", text]); // Cut the text from {{to}} let name = this.scanText("}}"); // Determine whether the text {{to}} is a variable or an array if (/ [#] /.test(name)) { res.push([name.substring(0.1), name.substring(1)]); } else { // If there are no variables after}}, there is no value for name. name && res.push(["name", name]); }}return res; } Copy the code
Test the current array parsing
let str = ` {{#arr}} {{name}} {{/arr}} `; let data = { arr:[ {name:1 "test"}, {name:"The test 2"}]};let scanner = new ScanUtil(str); console.log(scanner.compileStrToTokens(),"scanner.compileStrToTokens()") Copy the code
The output
-
Store all items in the third element of the array [‘#’,’arr’,[]]
Store items 2, 3, and 4 in the third element of the array, where the algorithm is a little more complicated.
Considering that there may be multiple levels of nested structure in the template string, recursion is used and an array of stack results is simulated to store the level of the array to which the current element should be inserted.
/ / [,1,1, 1 #, #, 1, 1, 1, 1, /, /] = > [1,1,1, [#, [1, 1, [#, 1, 1]]]] export function nestTokens(tokens) { // Get the last element of the array without modifying the original array const _getLastEle = (arr) = > [...arr].pop(); // The result array stores the returned results let resTokens = []; / / stack array let sections = [resTokens]; // Every element except/needs to be pushed into the result array for storage, but not directly into the result array, but into a collector that references either the result array or an element of the result array let collector = _getLastEle(sections); tokens.forEach((token) = > { switch (token[0]) { // When an array is encountered, store the node, then modify the collector's pointing, and store the collector case "#": collector.push(token); collector = token[2] = []; // push, which is used to assign to the next collector sections.push(collector); break; // The current collector is removed from the stack, and the collector at the top of the stack is fetched case "/": sections.pop(); collector = _getLastEle(sections); break; // Collector collects the element default: collector.push(token); break; }});return resTokens; } Copy the code
I’m going to modify the function compileStrToTokens
// Loop through, return tokens array compileStrToTokens() { let res = []; while (!this.eos) { // intercepts the text before {{ let text = this.scanText({{" "); // Add the result to the array res.push(["text", text]); // Cut the text from {{to}} let name = this.scanText("}}"); // Determine whether the text {{to}} is a variable or an array if (/ [#] /.test(name)) { res.push([name.substring(0.1), name.substring(1)]); } else { // If there are no variables after}}, there is no value for name. name && res.push(["name", name]); }}return this.nestTokens(res); } Copy the code
So the result is going to be zero
Parsing template strings into multi-dimensional arrays is done so far, and multi-level nesting of template strings is supported.
2. Compile multidimensional arrays into target strings
Compiling multidimensional arrays into target strings is a little easier. Just convert the second element of each item of the multidimensional array to a value and add it. If it is an array, the function is called recursively by iterating through the array.
export function compileTokensToStr(tokens, data) {
return tokens.reduce((res, token) = > {
let type = token[0];
let value = token[1];
// If it is a text node, add it directly
if (type === "text") {
res += value;
}
// If it is an array, call it recursively
else if (type === "#") {
res += data[value].map(item= > compileTokensToStr(token[2], item)).join("");
}
// If it is a variable, add the value from data, if there is {{.}} to indicate the current element
else {
res += _isDot(value) ? data : data[value];
}
return res;
}, "");
}
Copy the code
The final test code
let str = ` {{#arr}} {{name}} {{/arr}} `;
let data = {
arr: [{name: 1 "test"},
{name: "The test 2"},]};let scanner = new ScanUtil(str);
let tokens = scanner.compileStrToTokens();
console.log(tokens, "scanner.compileStrToTokens()");
let aimStr = compileTokensToStr(tokens,data);
console.log(aimStr, "aimStr");
Copy the code
The test results
The last
Mustache library is very powerful, handling not only array forms, but also booleans, functions, etc. We’re only dealing with arrays here, but the logic is intact. With Mustache, you learn a way to work with strings, using Pointers to iterate over strings and stacks to merge arrays.