Практический XSLT. Использование в качестве шаблонизатора

Автор: Fade
Источник: habrahabr

В сети доступно масса документации по языку XSL. Данный раздел не претендует на роль документации по языку, а лишь кратко, по шагам объясняет, как создать свой XSLT-шаблон.

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

Рабочий стол

Определим, что нам нужно для работы:
  • Входной XML-документ
  • XHTML-макет шаблона
  • Парсер XML для склейки XML с XSL

У меня входной XML документ выдает CMS-система, в которой каждая страница с материалом собирается в XML-дерево.

К XHTML-макету никаких ограничений нет. Есть лишь определенные рекомендации по верстке, которые позволят значительно сэкономить время на формирование шаблона.

В качестве парсера (сборщика) конечного документа можно использовать браузер. Нужно лишь указать в XML-документы путь к файлу шаблону:
<?xml-stylesheet type="text/xsl" href="template.xsl" ?>

Хотя, как показала практика, этот механизм довольно глючный (мне пришлось пользовать IE). Лучше воспользоваться средствами XML-парсинга языка, на котором написана CMS-система. Я использую Parser (на нем, вообщем-то, у меня вся система и работает).

Входной XML-документ

Для начала разберемся со входным XML-документом. Для того, чтобы использовать XSL нужно иметь полное представление о его структуре.

Я использую следующую схему:

<?xml version="1.0" encoding="windows-1251"?>
<document lang="ru" id="0">
<header>
<title>Начало</title>
</header>
<lang_table>
<item hit="yes" lang="ru" title="Russian"/>
</lang_table>
<navigation>
<sections>
<item id="0" parent_id="0" is_published="1" section="1">
<title>Начало</title>
<dir>/</dir>
</item>
<item id="1" parent_id="0" is_published="1" section="1">
<title>Новости</title>
<dir>news</dir>
</item>
</sections>
</navigation>
<content>
<item id="1" container="1" sorting="2" type="com" method="list_news" title="Новости"></item>
</content>
</document>

Обозначенный выше пример схемы не претендует на свою оптимальность. В силу тех или иных причин, мне он удобен. Но, обо всем по порядку.

<?xml version="1.0" encoding="windows-1251"?> - заголовок XML-файла. Должен идти строго с начала файла. В нем прописана версия используемого XML-языка и кодировка документа. Я как правило работаю в windows-1251 (пока так удобнее), но, по идее UTF-8 лучше.

<document lang="ru" id="0"> - корневой элемент документа (можно придумать свое имя). Атрибуты:
  • Lang - язык документа. Нужен для создания мультиязычных шаблонов.
  • Id - идентификатор текущего раздела.

<lang_table> - таблица языков, используемых на сайте.
<navigation> - блок элементов навигации:
<sections> - блок основной навигации (основная структура сайта):
<item id="0" parent_id="0" is_published="1" section="1"> - элемент структуры сайта. Атрибуты:
  • Id - идентификатор раздела.
  • Parent_id - идентификатор родительского раздела.
  • Is_published - опубликован ли раздел.
  • Dir - uri-адрес раздела. По нему формируются полные адреса.
  • Section - тип раздела. Используется если необходимо разбить меню на основное и сервисное.

<content> - блок содержимого.
В моей CMS используется модульная структура: все наполнение сайта представляет собой модули двух видов:

  • Html - текстовый модуль. Статические модули, которые заполняет редактор сайта.
  • Com - модуль-компонента. Динамические модули, которые формируют различные программные модули CMS: новости, статистика, поисковые блоки и т.д.

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

CMS при сборке страницы просто выводит в все модули, которые задействованы на странице в виде: <item id="1" container="1" sorting="2" type="com" method="list_news" title="Новости">
Атрибуты:
  • Id - идентификатор модуля.
  • Container - блок-назначение (в каком блоке шаблона выводиться).
  • Sorting - порядок вывода в блоке.
  • Type - тип:
    • Com - модуль-компонентаю
    • Html - текстовый модуль.

  • Method - обработчик данных.
  • Title - название модуля.

DTD я практически не использую (лишь в самом общем виде):

<!DOCTYPE site_page [
<!-- Character entity references for ISO 8859-1 characters -->
<!ENTITY nbsp "&#160;">
<!ENTITY sect "&#167;" >
<!ENTITY copy "&#169;">
<!ENTITY laquo "&#171;">
<!ENTITY reg "&#174;">
<!ENTITY deg "&#176;">
<!ENTITY plusmn "&#177;">
<!ENTITY para "&#182;">
<!ENTITY raquo "&#187;">
<!ENTITY times "&#215;">

<!-- Character entity references for symbols, mathematical symbols, and Greek letters -->

<!ENTITY bull "&#8226;">
<!ENTITY hellip "&#8230;">

<!-- Character entity references for markup-significant and internationalization characters -->

<!ENTITY ndash "&#8211;">
<!ENTITY mdash "&#8212;">
<!ENTITY lsquo "&#8216;">
<!ENTITY rsquo "&#8217;">
<!ENTITY sbquo "&#8218;">
<!ENTITY ldquo "&#8220;">
<!ENTITY rdquo "&#8221;">
<!ENTITY bdquo "&#8222;">
<!ENTITY lsaquo "&#8249;">
<!ENTITY rsaquo "&#8250;" >
<!ENTITY euro "&#8364;">
]>

Его можно вставить прямо в XML-документ. Сразу после <?xml version="1.0" encoding="windows-1251"?>.

Подготовка XHML-шаблона


XSL-шаблон создается на базе XHTML-шаблона (некой типовой страницы сайта). Код XHTML-страницы, при этом, должен быть валидным.

Рассмотрим по шагам процесс создания шаблона.

Проверив валидность XHML-страницы своего шаблона, для облегчения собственной работы, обозначьте в нем положение всех динамических блоков:
  • Меню (и других элементов навигации).
  • Информационных блоков страницы - то место в шаблоне, в котором будут выводиться модули сайта.
  • Заголовка/названия страницы.

Сделать это лучше всего с помощью обычных HTML-комментариев:
...
<h1 class="top">
<!-- Название раздела -->
Администрирование сайта
<!-- /Название раздела -->
</h1>
...
<!-- меню -->
<ul id="main-menu">
<li><a href="#">Начало</a></li>
<li class="curent-item"><a href="#">Новости</a></li>
<li><a href="#">Разделы</a></li>
</ul>
<!-- /меню -->
...
<!-- блок левых модулей -->
Всякие новости
<!-- /блок левых модулей -->
...
<!-- Блок основного содержимого -->
Текст
<!-- /Блок основного содержимого -->
...


Основы описания XSL-шаблонов


Все файлы XSL-шаблонов имеют следующий вид:
<xsl:stylesheet version = `1.0` encoding="UTF-8"?>
<xsl:template match="element">
данные шаблона
</xsl:template>
</xsl:stylesheet>


Где: <xsl:stylesheet version = `1.0` encoding="UTF-8"?> - определяет тип XML-документа и кодировку. Я использую UTF-8 (не спрашивайте, почему).
<xsl:stylesheet> </xsl:stylesheet> - начало и конец XSL-документа.
<xsl:template match="element"> </xsl:template> - начало и конец шаблона для элемента element.

Шаблоны можно условно разделить на три вида:
  • <xsl:template match="element"></xsl:template> - шаблон, описывающий правила преобразования элемента element. Применяется автоматически ко всем элементам element.
  • <xsl:template match="element" mode="mode1"></xsl:template> - шаблон, описывающий правила преобразования элемента element в режиме mode1. Таким образом можно описать различные правила обработки элементов element.
  • <xsl:template name="template-name"></xsl:template> - шаблон с именем template-name. Не имеет привязки к какому-либо элементу XML-документа.

Если элементы одного вида могут встречаться в различных частях структуры XML-документа (например, в XML-документе, формируемом системой элемент item используется повсеместно и имеет разное значение), то в шаблоне можно указать "структурный адрес" такого элемента:
<xsl:template match="navigation/sections/item"></xsl:template>
При этом, порядок применения шаблонов иерархичный, т.е., сначала шаблон применяется к корневому элементу, а затем, к дочерним, т.е. если мы вызвали обработчик для navigation, то для вызова обработчика для navigation/sections/item нам достаточно указать адрес sections/item.

Структура папок шаблонов


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

В простейшем варианте можно создать каталог xsl и там все складировать.

Далее, чтобы внутри этого каталог шаблоны не путались (для каждого шаблона у нас получиться несколько файлов) создадим вложенные каталоги:
  • template_folder - каталог с файлами шаблона. Называть ее можно по имени шаблона, например my_template.
  • dtd - файлы описания основных сущностей. Могут быть полезными.
  • lang - шаблоны сообщений для различных языков (если на сайте используется их используется несколько).
  • mod - шаблоны модулей.

Нам для начала потребуется создать каталог xsl/my_template и в нем, файл layout.xsl следующего вида:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet SYSTEM "../dtd/entities.dtd">
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- Основной шаблон -->
<xsl:template match="/node()">
</xsl:template>
<!-- / Основной шаблон -->
</xsl:stylesheet>

Где:
<xsl:template match="/node()"> </xsl:template> - шаблон для элемента /node() (корневого). Вместо /node() можно указать //document, т.к. он у нас являеться корневым узлом.

Копируем весь XHTML-код внутрь блока <xsl:template match="/node()"></xsl:template>

Этот шаблон будет автоматически применяться ко всему XML-документу. В нашем случае, XSL-преобразование заменит весь XML-код на XHTML-код вашего шаблона.

Далее, необходимо в директории XSL создать файл template.xsl (где, template - название вашего шаблона), в котором размещаем следующий код:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- общий файл шаблона -->
<xsl:import href=" my_template /layout.xsl"/>
</xsl:stylesheet>

Где:
<xsl:import href="my_template/layout.xsl"/>
Директива импорта внешнего XSL-файла (обрабатываеться XSL-процессором) из указанного файла. Путь к файлу указываем относительный.

Создание шаблона для основного навигационного меню


Наш предыдущий шаблон не обладает никакой динамикой, т.к. просто заменяет весь выходной XML-документ на код нашего шаблона.

Следующий шаг - создание шаблона для меню.

Меню навигации сайта строиться на основе его структуры, представленной в XML-документе в следующем виде:
<navigation>
<sections>
<item id="0" parent_id="0" is_published="1" section="1">
<title>Начало</title>
<dir>/</dir>
</item>
<item id="1" parent_id="0" is_published="1" section="1" hit="yes">
<title>Новости</title>
<dir>news</dir>
</item>
</sections>
</navigation>

Текущий раздел определяется по двум параметрам:
  • Атрибуту id у корневого элемента document - он всегда равен id текущего раздела.
  • Атрибуту hit у элемента item - если таковой имеется, то это значит, мы находимся на "главной странице раздела".

Соответственно, для того, чтобы вывести меню сайта необходимо создать шаблон для элементов:
  • sections - корневой элемент меню.
  • item - элемент меню.

При этом, необходимо учесть, что элементы item могут содержать другие элементы item, в том случае, если у раздела есть подразделы:
<item>
<item></item>
</item>


1. Создаем в директории xsl/my_template файл navigation.xsl следующего вида:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet SYSTEM "../dtd/entities.dtd">
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- Глобальное меню навигации -->
<xsl:template match="sections" mode="global_menu">
</xsl:template>
<!-- /Глобальное меню навигации -->
</xsl:stylesheet>


2. Вставляем в шаблон код нашего меню из файла layout.xsl:


<!-- Глобальное меню навигации -->
<xsl:template match="sections" mode="global_menu">
<ul id="main-menu">
<li class="curent-item"><a href="/">Начало</a></li>
<li><a href="/sections/">Разделы</a></li>
<li><a href="/news/">Новости</a></li>
</ul>
</xsl:template>
<!-- /Глобальное меню навигации -->


3. …а на его место в файле layout.xsl вставляем вызов нашего шаблона меню:


<!-- меню -->
<xsl:apply-templates select="navigation/sections" mode="global_menu"/>
<!-- /меню -->

Где:
select="navigation/sections" - относительный (относительно текущего) путь-адрес элемента. При этом, будут обработаны все элементы navigation/sections.

mode="global_menu" - используем шаблон с режимом global_menu. Это нам нужно на тот случай, если нужно будет выводить еще и сервисное меню, отдельно, или "хлебные крошки", или что-еще другое на основе одной и той же ветки навигации.

4. Плюс, добавим в файл layout.xsl директиву импорта файла шаблона navigation.xsl:


<xsl:import href="navigation.xsl"/>

5. Далее, создаем в файле navigation.xsl еще один шаблон, для обработки пунктов меню:


<!-- Обработка ссылок меню -->
<xsl:template match="item" mode="global_menu">
<li>
<a>
<xsl:call-template name="href_attribute"/>
<xsl:value-of select="title"/>
</a>
</li>
</xsl:template>
<!-- /Обработка ссылок меню -->

Где:
<xsl:call-template name="href_attribute"/> - вызов шаблона по имени. При этом шаблон не имеет привязки к элементу, т.е. вызывается произвольно.

<xsl:value-of select="title"/> - вставка-вывод значения элемента title текущего элемента. Если в параметре перед именем элемента поставить символ @ - выводиться будет значения атрибута текущего элемента.

6. Немного изменяем шаблон sections:


<!-- Глобальное меню навигации -->
<xsl:template match="sections" mode="global_menu">
<ul id="main-menu">
<xsl:apply-templates select="item" mode="global_menu"/>
</ul>
</xsl:template>
<!-- /Глобальное меню навигации -->

Где:
<xsl:apply-templates select="item" mode="global_menu"/> - обработка всех элементов item элемента sections. При этом, элементы item самих элементов item (sections/item/item) обрабатываться не будут, т.е. выводиться только один уровень меню разделов.

Мы вынесли обработку элементов item (пунктов меню) в отдельный шаблон. При этом, в нем мы добавили еще и вызов другого шаблона: <xsl:call-template name="href_attribute"/>

Этот шаблон будет формировать нормальные uri-ссылки для элементов нашего меню. О нем немного позже.

7. Теперь нам необходимо доделать меню,


чтобы оно учитывало, какой раздел является текущим. Для этого нам придется добавить условную обработку в наш шаблон элемента item:
<!-- Обработка ссылок меню -->
<xsl:template match="item" mode="global_menu">
<li>
<xsl:choose>
<!-- если Текущий раздел -->
<xsl:when test="descendant-or-self::*/@id = /node()/@id">
<xsl:value-of select="title"/>
</xsl:when>
<!-- если раздел не текщий -->
<xsl:otherwise>
<a>
<xsl:call-template name="href_attribute"/>
<xsl:value-of select="title"/>
</a>
</xsl:otherwise>
</xsl:choose>
</li>
</xsl:template>
<!-- /Обработка ссылок меню -->

Здесь мы сталкиваемся с новой конструкцией:
<xsl:choose>
<xsl:when></xsl:when>
<xsl:otherwise></xsl:otherwise>
</xsl:choose>

…которая, собственно, и задает условную обработку XML-элементов. В качестве параметра мы задаем условие: <xsl:when test="descendant-or-self::*/@id = /node()/@id">

В нашем случае это условие равенства атрибутов ID у корневого элемента (document) и текущего элемента (item), которое и определяет, является ли элемент текущим.

Внутри блока <xsl:when></xsl:when> располагается то, что выводиться в случае выполнения условия. В блоке <xsl:otherwise></xsl:otherwise> - если условие не выполняется.

8. Теперь, разберем шаблон href_attribute:


<!-- Обработка адреса ссылок меню -->
<xsl:template name="href_attribute">
<xsl:attribute name="href">
<xsl:text>/</xsl:text>
<xsl:for-each select="ancestor-or-self::item">
<xsl:value-of select="dir"/>
<xsl:text>/</xsl:text>
</xsl:for-each>
</xsl:attribute>
</xsl:template>
<!-- /Обработка адреса ссылок меню -->


Здесь мы сталкиваемся с инструкцией xsl:attribute. Она позволяет создавать атрибуты для элементов внутри которого она вызывается. В нашем случае мы вызываем ее из элемента a, соответственно, она создаст для него атрибут href, т.е. адрес.

Инструкция <xsl:for-each select="ancestor-or-self::item"> задает цикл обработки для всех элементов, удовлетворяющих условию. В нашем случае мы выбираем ancestor-or-self::item - ось элементов от корневого элемента до текущего по цепочке. В нашем случае это позволяет выбрать для всей цепочки узлы dir, т.е. построить полный адрес текущего узла-раздела.

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


Опубликовал admin
25 Мар, Вторник 2008г.



Программирование для чайников.