Пишем свой шаблонизатор на Python

от автора

Наверняка многие из вас задумывались о том, как устроены шаблонизаторы, какого его внутреннее устройство и каким образом происходит преобразование в фрагменты HTML-кода, однако не догадывались о каких-то особенностях его реализации. Поэтому давайте реализуем упрощенную версию движка шаблонов и продемонстрируем как это работает «под капотом».

Для начала необходимо определится с синтаксисом, с которым будет работать наш движок. Другими словами — конструкции языка, которые будет понимать шаблонизатор и обрабатывать их соответствующим образом.

Синтаксис языка

Хоть в нашем случае язык будет сильно упрощен, но для примера реализуем работу переменными и блоками, которые будут выглядеть примерно следующим образом:

<!—- переменные будет расположены внутри `{{` и `}}` --> <div>{{my_var}}</div>  <!—- блоки же окружены `{%` и `%}` --> {% each items %}     <div>{{it}}</div> {% end %} 

Большинство блоков должно быть закрыто, как в примере, приведенном выше и оканчиваться тегом end.
Наш шаблонизатор будет также поддерживать работу с циклами и условиями. Не забудем добавить поддержку вызовов внутри блоков – это будет достаточно удобной вещью, которая может нам пригодится.

Циклы

С их помощью мы сможем обходить коллекции и получать элемент, с которым будет совершать нужные операции:

{% each people %}     <div>{{it.name}}</div> {% end %}  {% each [1, 2, 3] %}     <div>{{it}}</div> {% end %}  {% each records %}     <div>{{..name}}</div> {% end %} 

В этом примере, people это коллекция и it ссылается на элемент из нее. Точка, как разделитель, позволяет обратится к полям объекта, чтобы извлечь необходимую информацию. Использование ".." предоставит доступ к именам, расположенных в контексте родителя.

Условия

Не нуждаются в представлении. Наш движок будет их поддерживать конструкцию if…else, а также операторы: ==, <=,> =, =, is, >, <!.

{% if num > 5 %}     <div>больше 5</div> {% else %}     <div>меньше или равно 5</div> {% end %} 
Вызовы функций

Вызовы должны быть указаны внутри шаблона. Не забудем, конечно же, поддержку именованных и позиционных параметров. Блоки, вызывающие функции, не должны быть закрытыми.

<!—- поддержка позиционных аргументов... --> <div class='date'>{% call prettify date_created %}</div> <!-- ...и именованных аргументов --> <div>{% call log 'here' verbosity='debug' %}</div> 

Теоретическая часть

Прежде чем углубляться в детали движка, который будет заниматься рендерингом шаблонов необходимо иметь представление о том, каким образом представлять шаблоны в памяти.

В нашем случае будут использоваться абстрактные синтаксические деревья (далее АСД), столь необходимые для представления данных. АСД – это результат лексического анализа исходного кода. Эта структура имеет много достоинств по сравнению с исходным кодом, одним из которых является исключение ненужных текстовых элементов (например, разделителей).

Мы будет производить парсинг данных и анализировать шаблон, строя соответствующее дерево, которое будет представлять некий скомпилированный шаблон. Рендеринг шаблона будет представлять собой простой обход по дереву, при котором будут возвращаться элементы дерева, сформированные в фрагменты HTML кода.

Определение синтаксиса

Первым шагом в нашем нелегком деле будет разделение контента на фрагменты. Каждый фрагмент – это тег HTML. Для разделение контента будут использоваться регулярные выражения, а также функция split().

VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % (     VAR_TOKEN_START,     VAR_TOKEN_END,     BLOCK_TOKEN_START,     BLOCK_TOKEN_END )) 

Итак, давайте проанализируем TOK_REGEX. В этом регулярном выражении у нас есть выбор между переменной или блоком. В этом есть определенный смысл – мы же хотим разделить содержимое по переменным или блокам. Обертка в виде тегов, которые были оговорены заранее, помогут нам определить фрагменты, которые нужно обработать. Знак ?, указанный внутри регулярного выражения – это не жадное повторение. Это необходимо для того, чтобы регулярное выражение было «ленивым» и останавливалось на первом совпадении, например, когда нужно извлечь переменные, указанные внутри блока. Кстати здесь можно почитать о том, как контролировать жадность регулярных выражений.

Вот простой пример, демонстрирующий работу данной регулярки:

>>> TOK_REGEX.split('{% each vars %}<i>{{it}}</i>{% endeach %}') ['{% each vars %}', '<i>', '{{it}}', '</i>', '{% endeach %}'] 

Кроме того, каждому обработанному фрагменту будет соответствовать свой тип, которые будут учитываться при обработке и компиляции. Фрагменты будут разделены на четыре типа:

VAR_FRAGMENT = 0 OPEN_BLOCK_FRAGMENT = 1 CLOSE_BLOCK_FRAGMENT = 2 TEXT_FRAGMENT = 3 

Формирование АСД

После анализа регулярным выражением исходного текста HTML-страницы, содержащей фрагменты, относящиеся к нашему шаблонизатору, необходимо построить дерево на основе элементов, которые относятся к нашему «языку». У нас будет класс Node, являющегося корнем дерева и содержащего дочерние узлы, которые являются подклассами для каждого типа узла. Подклассы должны содержать методы process_fragment() и render():
process_fragment() используется для дальнейшего анализа содержимого и хранения необходимых атрибутов объекта Node.
render() нужен для преобразования соответствующего фрагмента в HTML –код

Опционально будет реализовать методы enter_scope() и exit_scope(), которые вызываются в процессе работы компилятора. Первая функция, enter_scope(), вызывается когда узел создает новую область (об этом позже), и exit_scope() чтобы покинуть текущую область, обрабатываемой при завершении обработки области.

Базовый класс Node:

class _Node(object):     def __init__(self, fragment=None):         self.children = []         self.creates_scope = False         self.process_fragment(fragment)      def process_fragment(self, fragment):         pass      def enter_scope(self):         pass      def render(self, context):         pass      def exit_scope(self):         pass      def render_children(self, context, children=None):         if children is None:             children = self.children         def render_child(child):             child_html = child.render(context)             return '' if not child_html else str(child_html)         return ''.join(map(render_child, children)) 

А вот пример подкласса Variable:

class _Variable(_Node):     def process_fragment(self, fragment):         self.name = fragment      def render(self, context):         return resolve_in_context(self.name, context) 

При определения узла будет анализироваться фрагмент текста, который нам подскажет тип этого фрагмента (т.е. это переменная, скобка, и т.п.)
Текст и переменные будут преобразованы в соответствующие им подклассы.
Если же это циклы, то их обработка будет происходить немного дольше, ведь это означает целый ряд команд, которые нужно выполнить. Узнать что это блок команд достаточно просто: необходимо лишь проанализировать фрагмент текста, заключенного в «{%» и « %}». Вот простой пример:

{% each items %} 

Где each – это предполагаемый блок команд

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

def compile(self):     root = _Root()     scope_stack = [root]     for fragment in self.each_fragment():         if not scope_stack:             raise TemplateError('nesting issues')         parent_scope = scope_stack[-1]         if fragment.type == CLOSE_BLOCK_FRAGMENT:             parent_scope.exit_scope()             scope_stack.pop()             continue         new_node = self.create_node(fragment)         if new_node:             parent_scope.children.append(new_node)             if new_node.creates_scope:                 scope_stack.append(new_node)                 new_node.enter_scope()     return root 

Рендеринг

Последним шагом является преобразование АСД к HTML. Для этого мы посещаем все узлы дерева и вызываем метод render(). В процессе рендеринга необходимо учесть, с чем в данный момент происходит работа: с литералами или контекстом имени переменной. Для этого используем ast.literal_eval(), который безопасно позволяет проанализировать строку:

def eval_expression(expr):     try:         return 'literal', ast.literal_eval(expr)     except ValueError, SyntaxError:         return 'name', expr 

Если же имеем дело с контекстом имени переменной, то анализируем, что указано с ним: «.» или «..»:

def resolve(name, context):     if name.startswith('..'):         context = context.get('..', {})         name = name[2:]     try:         for tok in name.split('.'):             context = context[tok]         return context     except KeyError:         raise TemplateContextError(name) 

Заключение

Данная статья является переводом, которая позволяет дать общее представление о том, как работают шаблонизаторы. Хоть это и является простейшим примером реализации, но его можно использовать как базу для построения более сложных шаблонизаторов.

Полный исходный код, а также примеры использования можно посмотреть тут

ссылка на оригинал статьи http://habrahabr.ru/post/180935/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *