В
предыдущей статье мы разобрали основные аспекты построения шаблона с помощью
XSLT. Однако, для полноценного шаблона нужно не только выводить меню сайта, но также и текстовый материал документа.
Вывод HTML-контента из XML-документа
Ранее, мы определили, что все содержание разделов в
XML-документе публикуется в блоке
<content>
. При этом, каждый модуль содержимого помещается в узел
<item id="1" container="1" sorting="2" type="html" method="html" title="Новости">
.
Для простоты предположим что пока у нас нет на сайте никакого динамического наполнения: форумов, новостей и д.р. Соответственно, нам на данном этапе достаточно выбрать элементы
item у которых указан тип
html и раскидать их в шаблоне в соответствии с тем, к какому блоку они относятся (указано в атрибуте
container).
Добавим в шаблон
xsl/template.xsl следующий код:
<!-- Обработка HTML-методов/текстов -->
<xsl:template match="item" mode="html">
<h1><xsl:value-of select="@title"/></h1>
<xsl:value-of select="." disable-output-escaping="yes" />
</xsl:template>
<!-- /Обработка HTML-методов/текстов -->
Мы добавили шаблон для элементов
item. В соответствии с ним мы будем для каждого текстового блока выводить его название (атрибут
title) и сам текст. Для вывода текста мы воспользуемся инструкцией
<xsl:value-of select="." disable-output-escaping="yes" />
, в которой мы выбираем текущий элемент (
.) и с помощью параметра-атрибута
disable-output-escaping мы отключаем "маскирование" входного кода, т.е. текст будет выводиться "как есть". Это нам необходимо для корректного вывода тегов в коде форматирования текста.
Однако, как правило, наполнение сайта делают люди, которых мало волнует соответствие форматирования текста стандарту
XHTML. В случае с использованием
XSLT любой незакрытый тэг (тот же
<br>
) будет вызывать ошибку парсера. Я долгое время пытался придумать разного рода анализаторы кода для чистки вводимого контента (тех-же новостей) на сайт, пока
Денис Креминский не подсказал мне одно простое, но весьма эффективное решение: поместить весь опасный код в
CDATA.
Секция CDATA используется для того, чтобы обозначить части документа, которые не должны восприниматься как разметка. Секция CDATA начинается со строки `<![CDATA[` и заканчивается строкой `]]>
`. Внутри самой секции не должна присутствовать строка `]]>
`.
Другими словами, все что в
CDATA XSL-парсером не анализируется и передается в конечный документ "как есть".
Теперь, нам нужно указать где нам нужно выводить те или иные модули. Для этого, откроем
xsl/my_template/layout.xsl и найдем в где у нас начинается разбивка на блоки.
Предположим, что у вас в изначальном
XHTML-шаблоне была следующая таблица:
<table id="content-table">
<tr>
<td id="content-left">
<!-- Основной блок текста -->
</td>
<td id="content-right">
<!-- Дополнительный блок текста -->
</td>
</tr>
</table>
В ней описывается простой двух-колоночный макет. Вырежем данный код (всю таблицу) и поместим ее в файл
xsl/template.xsl в новый шаблон:
<!-- Описание блока содержимого для данного стиля страницы -->
<xsl:template match="content">
<table id="content-table">
<tr>
<td id="content-left">
<!-- Основной блок текста -->
</td>
<td id="content-right">
<!-- Дополнительный блок текста -->
</td>
</tr>
</table>
</xsl:template>
<!-- / Описание блока содержимого для данного стиля страницы -->
Мы создали шаблон для блока
content. Соответственно, в основном файле
xsl/my_template/layout.xsl нам нужно указать, где он должен вызываться. Для этого вставьте в на место вырезанной таблицы следующий код:
<!-- таблица модулей -->
<xsl:apply-templates select="content"/>
<!-- /таблица модулей -->
Осталось только добавить в
xsl/template.xsl инструкции для вывода нужных нам блоков:
<!-- Описание блока содержимого для данного стиля страницы -->
<xsl:template match="content">
<table id="content-table">
<tr>
<td id="content-left">
<!-- Основной блок текста -->
<xsl:apply-templates select="item[@container = 1]" mode="html"/>
</td>
<td id="content-right">
<!-- Дополнительный блок текста -->
<xsl:apply-templates select="item[@container = 2]" mode="html"/>
</td>
</tr>
</table>
</xsl:template>
<!-- / Описание блока содержимого для данного стиля страницы -->
В результате, из нашего
XML-документа основное наполнение страницы будет аккуратно разложено в соответствующие блоки шаблона.
Создание под-меню
Разберемся теперь более подробно со структурой нашего сайта.
В нашем
XML-документе в блоке
<sections>
выводиться все дерево-структура сайта. При этом, для каждого элемента (раздела сайта) у нас предусмотрен атрибут section который задает тип раздела. Однако, наш шаблон для меню навигации пока никак это не учитывает.
Откроем
xsl/navigation.xsl и доработаем немного шаблон
<xsl:template match="sections" mode="global_menu">
:
<!-- Глобальное меню навигации -->
<xsl:template match="sections" mode="global_menu">
<div id="menu-main">
<ul>
<xsl:apply-templates select="item[@section=1]" mode="global_menu"/>
</ul>
</div>
</xsl:template>
<!-- /Глобальное меню навигации -->
Мы указали, что обрабатывать будем только разделы, у которых указан тип
section=1.
Далее, попробуем избавиться от конструкции
<xsl:when test="descendant-or-self::*/@id = /node()/@id">
(с помощью нее мы определяли является ли текущим обрабатываемый пункт меню). Ничего плохого в этой конструкции нет, но мы можем ее упростить и сделать более универсально (может кому-то неудобно указывать номер текущего раздела в корневом узле и постоянно его проверять): попробуем передавать
id текущего раздела в виде переменной.
Допустим, блок
<sections>
во входном
XML-документе формируется отдельным модулем
CMS и этот модуль указывает номер текущего раздела в корневом узле блока
<sections>
:
<sections hit_id="2">
. Таким образом, в шаблоне для элемента
sections перед вызовом обработчиков для
item нам нужно им как-то передать номер текущего раздела. Для этого воспользуемся переменными. Переделаем немного строку вызова обработчиков для
item:
<xsl:apply-templates select="item[@section=1]" mode="global_menu">
<xsl:with-param name="cur"><xsl:value-of select="@hit_id"/></xsl:with-param>
</xsl:apply-templates>
Мы поместили внутрь вызова
<xsl:apply-templates/>
(сделав из него парный элемент) инструкцию
<xsl:with-param name="cur">
, которая создает параметр-переменную cur в контексте текущего вызова.
Теперь, упростим немного шаблон
<xsl:template match="item" mode="global_menu">
:
<!-- если Текущий раздел -->
<xsl:when test="descendant-or-self::*/@id = $cur">
<a><xsl:value-of select="title"/></a>
</xsl:when>
В проверку мы вставили переменную
$cur. Некоторые, наверное спросят: а зачем нам
descendant-or-self::*/@id, если можно сравнивать просто с
@id, т.е. просто идентификатором текущего раздела? Дело в том, что если у раздела есть подразделы и мы попадаем на них, то нам нужно как-то отметить родительский раздел. Конструкция
descendant-or-self::*/@id указывает совпадение атрибута
id для узла или любого из его потомков.
Однако, нам нужно предусмотреть два варианта:
- когда мы находимся в самом разделе - ссылка на раздел не нужна;
- когда мы находимся в подразделе - ссылку на раздел делаем.
При этом, в любом случае, нужно как-то выделить текущий раздел на фоне остальных:
<!-- Обработка ссылок меню -->
<xsl:template match="item" mode="global_menu">
<li>
<xsl:choose>
<!-- если выбран раздел или его подраздел -->
<xsl:when test="descendant-or-self::*/@id = $cur">
<xsl:attribute name="id"><xsl:text>cur</xsl:text></xsl:attribute>
<a>
<!-- если выбран под-раздел раздела - ставим ссылку на сам раздел -->
<xsl:if test="descendant::*/@id = $cur">
<xsl:call-template name="href_attribute"/>
</xsl:if>
<xsl:value-of select="title"/>
</a>
</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:if test="descendant::*/@id = $cur"></xsl:if>
Инструкция
xsl:if позволяет реализовывать условие. В отличие от
xsl:choose, можно обозначить шаблон только для случая, когда условие выполняется. В нашем случае, мы проверяем
descendant::*/@id - совпадение
id дочерних элементов с переменной
$cur.
Для полноты нашего меню нам нужно где-то вывести подразделы (если таковые имеются) для выбранного раздела.
Для простоты, выведем дерево всех подразделов текущего раздела в правой колонке нашего шаблона. Для этого откроем
xsl/template.xsl и немного изменим шаблон
<xsl:template match="content">
:
<!-- Описание блока содержимого для данного стиля страницы -->
<xsl:template match="content">
<table id="content-table">
<tr>
<td id="content-left">
<!-- Основной блок текста -->
<xsl:apply-templates select="item[@container = 1]" mode="html"/>
</td>
<td id="content-right">
<!-- подменю -->
<xsl:if test="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]/item/@id">
<div id="menu-sub">
<xsl:apply-templates select="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]" mode="sub-menu">
<xsl:with-param name="cur"><xsl:value-of select="//navigation/sections/@hit_id"/></xsl:with-param>
</xsl:apply-templates>
</div>
</xsl:if>
<!-- Дополнительный блок текста -->
<xsl:apply-templates select="item[@container = 2]" mode="html"/>
</td>
</tr>
</table>
</xsl:template>
<!-- / Описание блока содержимого для данного стиля страницы -->
Добавили проверку существования дочерних элементов у текущего раздела:
<xsl:if test="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]/item/@id">
При этом учитывается то обстоятельство, что текущим может быть подраздел раздела.
Далее, нам нужно создать вызываемый шаблон:
<xsl:apply-templates select="item" mode="sub-menu">
Для этого, добавить следующий код в
xsl/my_template/navigation.xsl:
<!-- Подменю -->
<xsl:template match="item" mode="sub-menu">
<ul>
<xsl:apply-templates select="item" mode="sub-menu-items">
<xsl:with-param name="cur"><xsl:value-of select="$cur"/></xsl:with-param>
</xsl:apply-templates>
</ul>
</xsl:template>
<!-- Подменю -->
Обратите внимание, что в отличии от основного меню мы начинаем обработку не с элемента
<sections>
а с элемента
<item>
, у которого есть дочерние элементы. В остальном, данный шаблон почти полностью повторяет шаблон основного меню.
Такой универсальности позволяет добиться CSS и верстка с помощью слоев. Я до сих пор в отдельных случаях использую таблицы для верстки, но постепенно стараюсь переходить на DIV-верстку, т.к. она позволяет значительно сократить код и, что более важно для XSLT, сделать шаблоны универсальными. Например, для большей части последних сайтов мне не пришлось переделывать navigation.xsl.Однако, отличия в подменю все-же есть. В том, что оно может иметь много уровней. Соответственно, шаблон
<xsl:apply-templates select="item" mode="sub-menu-items">
также будет отличаться от соответствующего шаблона для основного меню разделов:
<!-- Элементы подменю -->
<xsl:template match="item" mode="sub-menu-items">
<li>
<xsl:choose>
<!-- если выбран раздел или его подраздел -->
<xsl:when test="descendant-or-self::*/@id = $cur">
<xsl:attribute name="id"><xsl:text>cur</xsl:text></xsl:attribute>
<!-- если выбран под-раздел раздела - ставим ссылку на сам раздел -->
<xsl:if test="descendant::*/@id = $cur">
<a><xsl:call-template name="href_attribute"/></a>
</xsl:if>
<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>
<!-- если есть поразделы -->
<xsl:if test="item/@id">
<ul>
<xsl:apply-templates select="item" mode="sub-menu-items">
<xsl:with-param name="cur"><xsl:value-of select="$cur"/></xsl:with-param>
</xsl:apply-templates>
</ul>
</xsl:if>
</li>
</xsl:template>
<!-- Элементы подменю -->
Отличие одно - мы обрабатываем дочерние элементы
item для каждого
item, рекурсивно вызывая шаблон
<xsl:template match="item" mode="sub-menu-items">
.
Продолжение следует…P.S. Подготовка статьи отнимает массу времени. Однако, эта работа позволила мне взглянуть свежим взглядом на свои старые XSL-шаблоны.