130 строк кода для написания шаблонизатора

Python

Шаблонный движок как я понимаю

Проще говоря, механизм шаблонов должен определитьшаблон, то кормить егоданные, будет сгенерирована соответствующая структура html.шаблонпредставляет собой предопределенную строку, структуру, похожую на HTML, с вкраплениями некоторых управляющих операторов (if, for и т. д.), Например:

<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>

данныеЭто данные в формате json, данные подаются разные, сгенерированный html также отличается, например

{
    '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
        }
    ]
}

будет генерировать html следующим образом:

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

Это отражаетДанные и представленияИдея разделения, это очень удобно, чтобы изменить любую сторону позже.

сделать

Что нам нужно сделать, так это сгенерировать соответствующий html на основе известного шаблона и данных, чтобы мы могли определить такую ​​функцию, которая имеет два параметра и возвращаемое значение, Параметры соответствуютшаблониданные, а возвращаемое значение соответствует окончательному html. Прототип функции выглядит следующим образом:

def TemplateEngine(template, context):
    ...
    return html_data

шаблон типа str, контекст типа dict, html_data также типа str

поддерживаемый синтаксис

Поскольку я часто использую разработку Django в своей работе, я знаком с механизмом шаблонов Django, поэтому для объяснения я буду использовать синтаксис, поддерживаемый Django. На самом деле, если говорить прямо, вообще два синтаксиса,{{ }}и{% %}.{{ }}Он содержит (переменную) переменную, данные берутся из контекста, и все будет заменено соответствующими данными в контексте, Как и в предыдущем примере, {{ user_name }} в конечном итоге будет заменено на Jack.{% %}структура управления, существует четыре вида:{% if %},{% for %},{% endif %},{% endfor %}.{% if %},{% endif %}должны появляться парами, по тому же признаку,{% for %}, {% endfor %} также должны быть сопряжены.

Реализовать идеи

Обычно существует три способа реализации механизма шаблонов: тип замены, интерпретируемый тип и скомпилированный тип. Тип замены — простая замена строки, например, {{ user_name }} заменяется на Jack, что соответствует Следующий код:

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

Это самый простой и, как правило, наименее эффективный. И интерпретируемый, и скомпилированный типы генерируют соответствующий код (на Python), а затем запускают этот код напрямую, чтобы сгенерировать окончательный HTML-код, что сложно реализовать. Это немного сложнее, чем тип замены. В этой статье говорится только о типе замены.

Общая идея такова: мы начинаем свнешний слойПреобразование шаблонов в обычные строки, {{ }}, {% %}нарезанный кубиками,ПотомрекурсияОбработайте каждый блок и, наконец, объедините результат каждого подблока.соединениеВстаньте. Ключевые слова: нарезка, рекурсивная обработка и сплайсинг. Давайте пройдемся по каждому шагу по очереди.

нарезанный кубиками

Возьмите предыдущий пример,

<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>

Для удобства обработки максимально разрежем шаблон на части, чтобы каждый маленький блок был обычным строковым блоком, {{ }} блоком, {%if %} блоком, {%endif %} блоком, {% for % block } block, {% endfor %} в блоке Один, поскольку приведенный выше шаблон разрезается на:

['<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>']

Чтобы вырезать шаблон (тип str), как показано на рисунке выше (тип списка), читатель сразу подумает об использовании функции разделения, да. Но здесь лучше использовать функцию разделения регулярных выраженийre.split, код показан ниже:

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

Рекурсивная обработка

В предыдущем разделе (обрезке) у нас уже был список, в этом разделе нам нужно просто перебрать его. Мы перебираем этот список, если это обычный блок и не окружен блоками {% if %} и блоками {% for %}, тогда мы помещаем значение прямо в конец В результате ; аналогично, если это блок {{ }} и не окружен блоком {% if %} и блоком {% for %}, тогда мы вызываем VarEngine для разбора этого блока {{ }} и нажимаем проанализированный результат в окончательный результат; в случае блока {% if %}, Тогда не спешим вычислять, а пушим в стек, и пушим в этот стек последующие блоки, пока не встретимсясоответствующийБлок {% endif %}. Обнаружив блок {% endif %}, мы вызываем IfBlock для разбора этого блока. стек, и помещаем результат синтаксического анализа в окончательный результат; аналогично блоку {% if %}, если блок {% for %} пройден, то мы помещаем блок {% for %} в стек, а затем блок также помещается в этот стек до тех пор, пока не встретит соответствующий Блок {% endfor %}, после встречи с блоком {% endfor %}, мы вызываем ForBlock для анализа стека и помещаем результат анализа в окончательный результат. Код (после отсечения) выглядит следующим образом:

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] != '{%':
            # 普通块的处理
            result.append(item) if not is_if and not is_for else stack.append(item)
        elif item[:2] == '{{':
            # {{ }}块的处理
            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 %}块的处理
                stack.append(item)
                if not is_for:
                    is_if = True
                    times += 1
            elif expression_lst[0] == 'for':
                # {% for %}块的处理
                stack.append(item)
                if not is_if:
                    is_for = True
                    times += 1
            if expression_lst[0] == 'endif':
                # {% endif %}块的处理
                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 %}块的处理
                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

результат - это список, который является окончательным результатом

соединение

пройти черезРекурсивная обработкаВ этом разделе мы сохранили результаты выполнения каждого блока в списке результатов и, наконец, использовали функцию соединения для преобразования списка в строку для получения окончательного результата.

return ''.join(result)

Реализация каждого двигателя

существуетРекурсивная обработкаВ этом разделе мы использовали несколько классов VarEngine, IfBlock и ForBlock для обработки блоков {{ }}, стеков, состоящих из блоков {% if %}, и стеков, состоящих из блоков {% for %} соответственно. Реализация этих двигателей описана ниже.

Реализация VarEngine

код напрямую

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)

Здесь главное обратить внимание на обработку . и |, | представляет собой фильтр, . чаще всего используется для представления свойств объекта, таких как

{{ person.username | format_name }}

человек может представлять объект или экземпляр класса, имя пользователя является его свойством, имя_формата — это фильтр (функция), который будет обрабатывать значение, указанное слева (здесь имя пользователя), и возвращать Обработанное значение. Немного сложнее, могут быть следующие блоки {{ }}, такие как

{{ info1.info2.person.username | format_name1 | format_name2 | format_name3 }}

Класс VarEngine наследуется от класса Engine, а _do_dot_seq определяется в классе Engine:

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)

Функция _do_dot в основном используется для обработки ситуации .(dot), такой как {{ person.name }}, и возврата результата. Есть три параметра: key_words, context и stay_func, key_words это имя атрибута, например имя, контекст соответствует контексту (или объекту, экземпляру класса и т. д.), например человеку, stay_func, если атрибут является функцией, следует ли запускать функцию. Код очень простой, так что вот он.

Реализация 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)

Логика IfBlock тоже очень проста, то есть сначала определить истинно ли условие if (судит по IfEngine), если истинно, то будет рекурсивно (вызов recursive_traverse), если ложно, то вернет сразу пустое нить. Вот небольшой разговор о реализации IfEngine, в основном об обработке и, или, используя функцию eval, эта функция выполнит строку внутри, например, eval('True и True и True') вернет True.

Реализация 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]
        if len(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})

Здесь используется синтаксис Python для... в..., например {% для человека в лице %}.Как и в случае с IfBlock, здесь также используется рекурсивный (вызов recursive_traverse).

Суммировать

В этой статье используется 130 строк кода для реализации механизма шаблонов.Общая идея по-прежнему очень проста.Это не что иное, как поочередная обработка каждого блока и, наконец, объединение результатов обработки каждого блока (объединение). Ключ — это основы Твердые, такие как рекурсия, регулярные выражения и т.д. Кроме того, следует уточнить и распространенные (встроенные) функции Python, такие как repr, eval (хотя и не рекомендуется, но для понимания), str.join, getattr, callable, и т. д., эти Функции могут помочь вам делать больше с меньшими затратами. Исходный код этого раздела находится вgithubНа, добро пожаловать, чтобы дать звезду.