Усатый-полосатый

Однажды мне пришлось написать.. Ну не важно что именно, важно что при решении задачи нужно было использовать шаблоны (только не те, которые patterns, и не те которые generics, а те которые templates).
Писал я это все на питоне. И вот что я узнал (истины вообщем-то общеизвестные, просто я чайник в питоне, да и в шаблонах тоже).

Что такое шаблоны?

В двух словах — есть текст, они на 80% состоит из одних и тех же слов, но остальные 20% формируются на лету в зависимости от каких-то данных. Чтобы упростить генерацию этого текста создается шаблон, содержащий 80%, а для 20% переменного текста в тексте создаются маркеры, по которым потом специальный template engine подставляет соответствующие значения.

Обычно любители шаблонов ассоциируют их с вебом, где страницы (view) верстаются на лету в зависимости от данных (model). Но на самом деле круг применения шаблонов намного шире. Осторожно, есть много шаблонизаторов заточенных именно для веба, и не xml-образный синтаксис им не под силу.

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

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

Существующие велосипеды

Первое что приходит на ум для шаблонизации — это знакомый всем printf.
К тому же в питоне он поддерживает именованные поля.

# Выглядит просто
username='John'
print "Hello, %s" % (username) # --> 'Hello, John'
# Но с поворяющимися значениями выглядит хуже
print "Hello, %s. Bye, %s!" % (username, username) 
# Хорошо что есть выход
print "Hello, %(user)s. Bye, %(user)s!" % {user='John'}

Кроме того, средства языка предлагают нам класс string.Template:

t = string.Template("Hello $user. Bye $user!")
rendered = t.substitute(user='John')

Как видите, он аналогичен printf, может только синтаксис чуть попроще.

А теперь возвращаясь к реальности. Если не передать значение переменной при рендеринге шаблона, то получим exception. Есть еще метод `safe_substitute`, в котором если не задано значение поля $user, то в тексте оно так и останется — $user (зачем?!).

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

Все дороги ведут..

К mustache. Почему-то до сих пор я об этой библиотеке не слышал. А она очень даже хорошая. Это простой, очень простой шаблонизатор без логики (ну или почти без логики) доступный просто повсюду где есть рефлексия (в c++ обошлись без нее) — python, perl, ruby, javascript, go, c++, java, obj-c, clojure…

Итак, в шаблоне выделяются два понятия — тег и секция. Теги логики не содержат, секции — немножко содержат.

Вот примеры синтаксиса шаблонов:

First Name: {{first_name}}
Last Name: {{last_name}}
{{#has_address}}
Street: {{street}}
Building: {{building}}
{{/has_address}}

В этом шаблоне улица и дом будут включены в конечную строку, если значение has_address истинно. Да, усами эта библиотека называется потому что разделители шаблонных полей — это фигурные скобки-усы. Меня эти усы особенно порадовали, потому что шаблонизировал я код на Bash, и разделитель ‘$’ как в string.Template пришлось бы заменять все равно. Говорят, «усы» не подходят только если шаблонизировать TeX-код.

Подробнее разные плюшки Mustache описаны тут.

Сбриваем усы

А как это все работает внутри? Мне было важно, чтобы код шаблонизатора был максимально компактным. При этом 80% функций Mustache мне были не нужны. И я решил попробовать написать свой урезанный донельзя шаблонизатор, похожий на Mustache.

Итак, круг задач:
* подстановка значений тегов разных типов (bool, int, float, string, object)
* секции с флагом (рендерятся если флаг установлен)
* секции с инвертированным флагом (рендерятся если флаг сброшен)

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

Нам понадобятся регулярные выражения для поиска секции и тега. Тег — это просто идентификатор внутри усов. Секция с флагом — это «{{#….}} … {{/….}}». Секция с инвертированным флагом — «{{^….}} … {{/….}}»
Вот сами регулярные выражения (я разделил на части для читабельности)

Тег: 
"{{" + "([^/#]+?)" + "}}"

Секция: 
"{{" + "([#^])" + "([^#/}]+?)" + "}}" 
  + "(.+?)" +  
  + "{{/\2}}"

С тегом все просто — усы, между ними что-то без знаков #, / и ^.
А вот секция — это усы, потом # или ^, потом идентификатор, усы закрываются. Потом произвольный текст, а потом такой же идентификатор, в усах и начинающийся с ‘/’.

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

class Template:

	notag = ''

	def __init__(self):
		section = r"{{([#^])([^#/}]+?)}}(.+?){{/\2}}"
		tag = r"{{([^/#]+?)}}"
		self.section_re = re.compile(section, re.M|re.S)
		self.tag_re = re.compile(tag)

	def render_sections(self, template, keys):
		while True:
			match = self.section_re.search(template)
			if match is None: break

			section, flag, section_name, inner = match.group(0, 1, 2, 3)
			if not section_name in keys:
				template = template.replace(section, '')
			else:
				v = keys[section_name]
				if (flag == '^' and v == True) or (flag == '#' and v == False):
					template = template.replace(section, '')
				elif isinstance(v, collections.Callable):
					template = template.replace(section, 
					              v(self.render_tags(inner, keys)))
				else:
					template = template.replace(section, 
					                self.render_tags(inner, keys))
		return template

	def render_tags(self, template, keys):
		while True:
			match = self.tag_re.search(template)
			if match is None: break
			tag, tag_name = match.group(0, 1)
			template = template.replace(tag, 
			               "%s" % (keys.get(tag_name, self.notag)))
		return template

	def render(self, template, keys):
		t = self.render_sections(template, keys)
		r = self.render_tags(t, keys)
		return r

Вот и все. Использовать просто:


t = Template()
model = {
  'message' : 'hello',
  'wrap' : lambda text: '<b>'+text+'</b>' 
}

# <b>hello</b>
t.render("{{#wrap}}{{message}}{{empty_var}}{{/wrap}}", model)

model = { 'a' : 3 }
# a is 3
t.render("a is {{#a}}{{a}}{{/a}} {{^a}}not set{{/a}}", model)
# a is not set
t.render("a is {{#a}}{{a}}{{/a}} {{^a}}not set{{/a}}", None)

Итог

Мне этот класс облегчил задачу очень сильно. Может кому еще сгодится.

Вот думаю теперь бы кто написал простенький Mustache-шаблонизатор на C, а то тоже иногда встают задачи текст на лету генерить.

Реклама

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s