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

Чтобы Яндекс.Маркет был доволен и счастлив, ему надо предоставлять xml-файл в специальном формате (http://partner.market.yandex.ru/legal/tt/), в котором содержится информация о товарах и товарных группах.

Тестовый пример

Для примера мы возьмем два товара в двух товарных группах. Вот что мы хотим получить в результате:

<source lang="xml"> <?xml version="1.0" encoding="windows-1251"?> <!DOCTYPE ymlcatalog SYSTEM "shops.dtd"> <ymlcatalog date="2009-12-15 04:47"> <shop> <name>Блинчеги-онлайн</name> <company>Блинная фабрика № 1</company> <url>http://www.blinonline.tld/</url> <currencies> <currency id="RUR" rate="1"/> </currencies> <categories> <category id="1">Корневая группа</category> <category id="2" parentId="1">Подгруппа</category> </categories> <offers> <offer id="1" available="true"> <url>http://www.blinonline.tld/catalog/1/1</url> <price>666.00</price> <currencyId>RUR</currencyId> <categoryId>1</categoryId> <picture>product_1.jpg</picture> <delivery>true</delivery> <name>Мой первый товар</name> <description>Первый блин.. Очень неплохой блин, стоит купить!</description> </offer> <offer id="2" available="true"> <url>http://www.blinonline.tld/catalog/2/2</url> <price>999.00</price> <currencyId>RUR</currencyId> <categoryId>2</categoryId> <picture>product_2.jpg</picture> <delivery>true</delivery> <name>Усовершенствованный первый блин.</name> <description>Но стоит немного дороже. Покупайте лучше первый!</description> </offer> </offers> </shop> </ymlcatalog> </source>

Как видите - YML довольно прост. Чтобы сварганить его, мы применим тяжелую артиллерию - библиотеку <code>cl-closure-template</code>, которую archimag сваял на лиспе всего за неделю, уложившись менее чем в 1000 строк кода, в то время как программисты Google писали на java и написали целых 15000 строк, и это, насколько я понял, даже без учета тестов.

Чтобы установить ее, я скачал архив с гитхаба, распаковал, положил в <code>/usr/share/common-lisp/source/closure-template</code> и создал симлинк на <code>closure-template.asd</code> в <code>/usr/share/common-lisp/systems</code>. После этого для использования надо сказать <code>(asdf:operate 'asdf:load-op '#:closure-template)</code> что гораздо правильнее, чем <code>(reqire 'closure-template)</code>

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

Шаблон

<source lang="xml"> {namespace yml} {template xml} <?xml version="1.0" encoding="windows-1251"?>{\n} <!DOCTYPE ymlcatalog SYSTEM "shops.dtd">{\n} <ymlcatalog date="{$datetime}">{\n} <shop>{\n} <name>{$marketname}</name>{\n} <company>{$marketcompany}</company>{\n} <url>{$marketurl}</url>{\n} <currencies>{\n} <currency id="RUR" rate="1"/>{\n} </currencies>{\n} <categories>{\n} {foreach $category in $categoryes} {call category data="$category" /} {/foreach} </categories>{\n} <offers>{\n} {foreach $offer in $offers} {call offer data="$offer" /} {/foreach} </offers>{\n} </shop>{\n} </ymlcatalog>{\n} {/template}

{template category} <category id="{$id}"{if $parent != 0} parentId="{$parent}"{/if}>{$name}</category>{\n} {/template}

{template offer} <offer id="{$id}" available="true">{\n} <url>http://www.blinonline.tld/catalog/%7B$category%7D/%7B$id}</url>{\n} <price>{$price}</price>{\n} <currencyId>RUR</currencyId>{\n} <categoryId>{$category}</categoryId>{\n} <picture>http://www.blinonline.tld%7B$picture/}</picture>{\n} <delivery>true</delivery>{\n} <name>{$name}</name>{\n} <description>{$description}</description>{\n} </offer>{\n} {/template} </source>

Разберем, что здесь происходит. Директива <code>{namespace имяпакета}</code> создает пакет, куда будут помещаться функции. Всего в пакете будут находится три функции: <code>xml</code>, <code>category</code> и <code>offer</code>, но вызывать мы будем лишь xml. Функция <code>xml</code> вызовет <code>category</code> и <code>offer</code> сама, когда ей потребуется отобразить группу товаров и товар.

Присмотритесь, как элегантно и очевидно это происходит. В блоке categoryes мы итерируемся по списку категорий, для каждой из которых вызывается функция <code>category</code> (в единственном числе), которой передается параметр <code>$category</code>. Теперь спустимся чуть пониже, чтобы рассмотреть блок <code>{template category}</code>. Он заполняет элемент категории, причем атрибут parentId отображется только для категорий, имеющих ненулевого родителя. Заполненные блоки возвращаются в xml. Точно также происходит заполнение offers. Более подробно о возможностях closure-template можно узнать в спецификации.

Код

Теперь о том, как передать данные в шаблон. Посмотрите на этот код:

<source lang="lisp"> ;;;; use closure-template for generate yml (asdf:operate 'asdf:load-op '#:closure-template) (closure-template:compile-template :common-lisp-backend #P"tpltest.txt") (defpackage #:ymf (:use #:cl)) (in-package #:ymf)

(defun get-date-time () (multiple-value-bind (second minute hour date month year) (get-decoded-time) (declare (ignore second)) (format nil "~d-~2,'0d-~2,'0d ~2,'0d:~2,'0d" year month date hour minute)))

(format t "~%~a" (yml:xml (list :datetime (get-date-time)

:marketname "Блинчеги-онлайн"

:marketcompany "Блинная фабрика № 1"

:marketurl "http://www.blinonline.tld/"

:categoryes '((:id 1

:parent 0

:name "Корневая группа") (:id 2

:parent 1

:name "Подгруппа"))

:offers '((:id 1

:category 1

:price "666.00"

:picture "/images/product1.jpg"

:name "Мой первый товар"

:description "Первый блин.. Cтоит купить!") (:id 2

:category 2

:price "999.00"

:picture "/images/product2.jpg"

:name "Усовершенствованный первый блин."

:description "Покупайте лучше первый!"))))) </source>

Как видите, мы вызываем функцию <code>xml</code> пакета ymf (помните про <code>{namespace ymf}</code>?), передавая ей список свойств (plist), содержащий все необходимые параметры. Выглядит гениально просто, не правда ли?

Надеюсь, мой пост вдохновит вас на использование cl-closure-template для самых разных задач.