I understand the template engine

Simply put, a template engine defines a template, feeds it data, and generates the corresponding HTML structure. A template is a predefined string that is an HTML-like structure interspersed with control statements (if, for, etc.), such as the following:

<p>Welcome, {{ user_name }}! </p> {% if is_show %} Your name: {{ user_name }} {% endif %} <p>Fruits:</p> <ul> {% for product in product_list %} <li>{{ product.name }}:{{ product.price }}</li> {% endfor %} </ul>Copy the code

The data is JSON data, and the HTML generated varies depending on the data fed, for example

{
    'user_name': 'Jack'.'is_show': True.'product_list': [{'show': True.'name': 'Apple'.'price': 20
        },
        {
            'show': False.'name': 'Pear'.'price': 21
        },
        {
            'show': True.'name': 'Banana'.'price': 22}}]Copy the code

The following HTML is generated:

<p>Welcome, Jack! </p> Your name: Jack <p>Fruits:</p> <ul> <li>Apple:20</li> <li>Banana:22</li> </ul>Copy the code

This reflects the idea of separating the data from the view, making it easy to modify either side later.

To do

All we need to do is generate HTML from the template and data we already know, so we can define a function that takes two parameters and a return value, one for the template and one for the data, and the return value for the final HTML. The function prototype is as follows:

def TemplateEngine(template, context):.return html_data
Copy the code

Template is STR, context is dict, and html_data is STR

Supported syntax

I’m familiar with Djangos template engine because I use Django a lot in my work, so I’ll use the syntax Django supports here. In fact, there are basically two syntax, {{}} and {% %}. {{}} contains a (variable) containing data from the context, which will be replaced by the corresponding data from the context, as in the previous example {{user_name}} will be replaced by Jack. {% %} is a control structure, there are four: if {% %}, {% for %}, {% endif %}, {} % endfor %. {% if %}, {% endif %} must appear in pairs, and {% for %}, {% endfor %} must also appear in pairs.

Implementation approach

There are basically three ways to implement a template engine, substitution, interpretation, and compilation. Substitution is a simple string substitution, such as {{user_name}} replaced by Jack, corresponding to the following code:

'{user_name}'.format(user_name = 'Jack')

Copy the code

This is the simplest and generally the least efficient. Both interpreted and compiled versions generate the corresponding (Python) code and then run the code directly to generate the final HTML, which is a bit more difficult to implement than the alternative version. This article will focus only on substitutions.

The general idea is this: we slice the template from the outermost layer into ordinary strings, {{}}, {% %}, then recursively process each block, and finally concatenate the results of each sub-block. The key words are: cutting, recursive processing, splicing. Let’s go through each step in turn.

cutting

Again, to take the previous example,

<p>Welcome, {{ user_name }}! </p> {% if is_show %} Your name: {{ user_name }} {% endif %} <p>Fruits:</p> <ul> {% for product in product_list %} <li>{{ product.name }}:{{ product.price }}</li> {% endfor %} </ul>Copy the code

For the convenience of processing, we cut the template as much as possible, so that each small piece is a normal string block, {{}} block, {% if %} block, {% endif %} block, {% for %} block, {% endfor %} block, such as the above template cut into:

['<p>Welcome, '.'{{ user_name }}'.'! 

'
.'{% if is_show %}'.'Your name: '.'{{ user_name }}'.'{% endif %}'.'<p>Fruits:</p><ul>'.'{% for product in product_list %}'.'<li>'.'{{ product.name }}'.':'.'{{ product.price }}'.'</li>'.'{% endfor %}'.'</ul>'] Copy the code

To cut a template (STR type) to the one shown above (list type), the reader immediately thinks of using the split function, and yes. But it’s best to use the regular expression split function re.split as follows:

tokens = re.split(r"(? s)({{.*? | {}} %. *? %})", template)
Copy the code

Recursive processing

We already got a list in the previous section (cutting), so in this section we just need to walk through it. We iterate over the list, and if it is a normal block and not surrounded by {% if %} and {% for %} blocks, we pusth the value directly into the final result; Similarly, if the {{}} block is not surrounded by {% if %} and {% for %} blocks, we call VarEngine to parse the {{}} block and pusth the parsed result into the final result; If it is an {% if %} block, we don’t evaluate it, but push it onto a stack, and push subsequent blocks onto the stack until the corresponding {% endif %} block is encountered. After the {% endif %} block is encountered, we call IfBlock to parse the stack and pusth the parse result into the final result; Similar to {% if %} blocks, if {% for %} blocks are traversed, we push {% for %} blocks onto a stack, and then push subsequent blocks onto the stack until the corresponding {% endfor %} block is encountered. Once {% endfor %} blocks are encountered, We call ForBlock to parse the stack and pusth the parse result into the final result. The code (after the clip) looks like this:

def recursive_traverse(lst, context):
    stack, result = [], []
    is_if, is_for, times, match_times = False.False.0.0
    for item in lst:
        if item[:2] != '{{' and item[:2] != '{%':
            # Normal block processing
            result.append(item) if not is_if and not is_for else stack.append(item)
        elif item[:2] = ='{{':
            # {{}} block handling
            result.append(VarEngine(item[2:2 -].strip(), context).result) if not is_if and not is_for else stack.append(item)
        elif item[:2] = ='{%':
            expression = item[2:2 -]
            expression_lst = expression.split(' ')
            expression_lst = [it for it in expression_lst if it]
            if expression_lst[0] = ='if':
                # {% if %} block handling
                stack.append(item)
                if not is_for:
                    is_if = True
                    times += 1
            elif expression_lst[0] = ='for':
                # {% for %} block handling
                stack.append(item)
                if not is_if:
                    is_for = True
                    times += 1
            if expression_lst[0] = ='endif':
                # {% endif %} block handling
                stack.append(item)
                if not is_for:
                    match_times += 1
                if match_times == times:
                    result.append(IfBlock(context, stack).result)
                    del stack[:]
                    is_if, is_for, times, match_times = False.False.0.0
            elif expression_lst[0] = ='endfor':
                # {% endfor %} block handling
                stack.append(item)
                if not is_if:
                    match_times += 1

                if match_times == times:
                    result.append(ForBlock(context, stack).result)
                    del stack[:]
                    is_if, is_for, times, match_times = False.False.0.0

Copy the code

Result is a list of final results

Joining together

Through the recursive processing section, we have stored the execution results of each block in the list result. Finally, we use the join function to convert the list into a string to get the final result.

return ' '.join(result)
Copy the code

Implementation of each engine

In the recursive processing section, we used classes VarEngine, IfBlock, and ForBlock to handle {{}} blocks, {% if %} stacks, and {% for %} stacks, respectively. The implementation of these engines is described below.

The realization of the VarEngine

Let’s go straight to the code

class VarEngine(Engine):
    def _do_vertical_seq(self, key_words, context):
        k_lst = key_words.split('|')
        k_lst = [item.strip() for item in k_lst]
        result = self._do_dot_seq( k_lst[0], context)
        for filter in k_lst[1:]:
            func = self._do_dot_seq(filter, context, True)
            result = func(result)
        return result
    def __init__(self, k, context):
        self.result = self._do_vertical_seq(k, context) if '|' in k else self._do_dot_seq(k, context)
Copy the code

The main thing here is to pay attention to processing. Filters, and | | said. The most commonly used said an object’s properties, such as

{{ person.username | format_name }}
Copy the code

Person can represent either an object or an instance of a class, username is its property, and format_name is a filter (function) that processes the value given on the left (in this case, username) and returns the processed value. More complex, there may be {{}} blocks like this

{{ info1.info2.person.username | format_name1 | format_name2 | format_name3 }}
Copy the code

The VarEngine class inherits from Engine, where _do_dot_seq is defined:

class Engine(object):
    def _do_dot(self, key_words, context, stay_func = False):
        if isinstance(context, dict):
            if key_words in context:
                return context[key_words]
            raise KeyNotFound('{key} is not found'.format(key=key_words))
        value = getattr(context, key_words)
        if callable(value) and not stay_func:
            value = value()
        return value
    def _do_dot_seq(self, key_words, context, stay_func = False):
        if not '. ' in key_words:
            return self._do_dot(key_words, context, stay_func)
        k_lst = key_words.split('. ')
        k_lst = [item.strip() for item in k_lst]
        result = context
        for item in k_lst:
            result = self._do_dot(item, result, stay_func)
        return repr(result)
Copy the code

The _do_dot function is primarily used to handle.(point) cases, such as {{person.name}}, and returns results. There are three arguments: key_words, context, and stay_func. Key_words is the property name, such as name; Context corresponds to a context (or object, class instance, etc.), such as person; Stay_func is whether to run the function if the property is a function. The code is very simple, so I’ll leave you there.

The realization of the IfBlock

class IfEngine(Engine):
    def __init__(self, key_words, context):
        k_lst = key_words.split(' ')
        k_lst = [item.strip() for item in k_lst]
        if len(k_lst) % 2= =1:
            raise IfNotValid
        for item in k_lst[2: :2] :if item not in ['and'.'or'] :raise IfNotValid
        cond_lst = k_lst[1:]
        index  = 0
        while index < len(cond_lst):
            cond_lst[index] = str(self._do_dot_seq(cond_lst[index], context))
            index += 2
        self.cond = eval(' '.join(cond_lst))

class IfBlock(object):
    def __init__(self, context, key_words):
        self.result = ' ' if not IfEngine(key_words[0] [2:2 -].strip(), context).cond else recursive_traverse(key_words[1:- 1], context)
Copy the code

IfBlock’s logic is simple: check if the condition is true (via IfEngine), if true, recurse (call recursive_traverse), and if false, return an empty string. The implementation of IfEngine uses eval(‘True and True and True’) to execute strings, such as eval(‘True and True’) to return True.

The realization of the ForBlock

class ForBlock(Engine):
    def __init__(self, context, key_words):
        for_engine = key_words[0] [2:2 -].strip()
        for_engine_lst = for_engine.split(' ')
        for_engine_lst = [item.strip() for item in for_engine_lst]
        iflen(for_engine_lst) ! =4:
            raise ForNotValid
        if for_engine_lst[0] != 'for' or for_engine_lst[2] != 'in':
            raise ForNotValid
        iter_obj = self._do_dot_seq(for_engine_lst[3], context)
        self.result = ' '
        for item in iter_obj:
            self.result += recursive_traverse(key_words[1:- 1], {for_engine_lst[1]:item})
Copy the code

This uses Python’s for syntax for… in… {% for person in persons %}, similar to IfBlock, is implemented with recursive_traverse.

conclusion

This article uses 130 lines of code to implement a template engine, the general idea is still very simple, nothing more than to process each block in turn, and finally join the results of each block processing. The key is to have a solid foundation in recursion, regular expressions, and other Python (built-in) functions such as repr, eval (not recommended, but know), str.join, getattr, Callable, and so on. These functions can help you do more with less. The source code of this section is on Github, welcome to give a star.