Привет всем!
Хочу рассказать о применении рекурсивных шаблонов в XSLT, так как многие начинающие работать с XSLT встречаются с задачами, которые требуют их применения и не знают, как такие задачи решать.
Возьмем пару обычных примеров:
1. Имеется нода со строкой, ее необходимо разбить на части по определенному символу (в нашем случае возьмем символ пробела) и каждую часть разукрасить в разные цвета.
2. Сделаем вывод номеров страниц (pager) исходя из того, что нам известно общее количество объектов (например, тем форума), количество объектов на странице и номер страницы, на которой мы в данный момент находимся.
Для начала приведу пример-основу, где мы выведем несколько чисел с использованием рекурсивных шаблонов:
Здесь условие в xsl:if, как многие уже догадались, предназначено для выхода из рекурсии.
Вызвав шаблон с помощью кода (входным XML можно указать любой валидный XML файл)
получим вывод всех чисел от 1 до 50 через пробел.
Тот, кто понял основную идею, сразу поймет, где это можно и нужно применять и может пропустить следующую часть статьи.
Итак, реальные примеры.
Имеем XML:
Необходимо разбить строку внутри ноды string по пробелам и каждый нечетный элемент вывести зеленым цветом, а каждый четный элемент — красным.
В XSLT/XPATH 2.0 имеется замечательная функция tokenize, которая может разбить строку на части и, соответственно, с помощью xsl:for-each мы можем пробежаться по ней и сделать с каждой частью, все что захотим. Но XSLT 2.0, насколько мне известно, хорошо поддерживается только процессором Saxon. Встроенные XSLT процессоры браузеров и libxslt в PHP его поддержки не имеют, поэтому будем применять рекурсивный шаблон.
Этот пример надуманный, вряд ли кому-то понадобится такая реализация, но очень похожие задачи встречаются нередко, поэтому решил показать именно на таком примере.
Это уже реально рабочий пример, который может кому-то понадобиться в работе, думаю, что с небольшими переделками каждый сможет использовать его в своих проектах.
Он получился довольно большой, потому что здесь не только используются рекурсивные шаблоны, но и наводятся всякие красивости, напрямую не относящиеся к рекурсивным шаблонам, но делающие результат наиболее приближенным к боевым условиям.
Итак, имеем XML вида:
Необходимо вывести номера страниц (pager). Перед и после номеров страниц нужно вывести переход на предыдущую и следующую страницу при условии, что мы находимся не на первой или не на последней странице. Также ставим условие, что необходимо вывести только ссылки для перехода на несколько ближайших страниц, а не на все сразу, поскольку тогда pager может получиться очень большим.
Получаем вот такой довольно большой шаблон:
В принципе, по комментариям в коде, все должно довольно легко пониматься, но если есть вопросы, с удовольствием отвечу на них.
Полезное чтиво: Алексей Валиков — Технология XSLT — русская библия XSLT :)
В будущем хочу написать об использовании ключей и режимов в XSLT, а также раскрыть кучу типичных мелких ошибок начинающих. Также попробую написать о производительности XSLT преобразований, но вряд ли это будет серьезная статья с кучей статистических выкладок, скорее всего просто рассмотрим узкие места производительности. Не могу дать никаких гарантий, насколько быстро появятся эти статьи, но постараюсь особо не тянуть с ними.
Хочу рассказать о применении рекурсивных шаблонов в XSLT, так как многие начинающие работать с XSLT встречаются с задачами, которые требуют их применения и не знают, как такие задачи решать.
Возьмем пару обычных примеров:
1. Имеется нода со строкой, ее необходимо разбить на части по определенному символу (в нашем случае возьмем символ пробела) и каждую часть разукрасить в разные цвета.
2. Сделаем вывод номеров страниц (pager) исходя из того, что нам известно общее количество объектов (например, тем форума), количество объектов на странице и номер страницы, на которой мы в данный момент находимся.
Для начала приведу пример-основу, где мы выведем несколько чисел с использованием рекурсивных шаблонов:
- <xsl:template name="numbers">
- <xsl:param name="current-number"/>
- <xsl:param name="max-number"/>
- <xsl:value-of select="$current-number"/>
- <!-- если не достигли последнего числа -->
- <xsl:if test="$current-number < $max-number">
- <!-- то выводим пробел после числа,
- поскольку необходимо будет вывести еще числа -->
- <xsl:text> </xsl:text>
- <!-- и вызываем самого себя для вывода следующего числа -->
- <xsl:call-template name="numbers">
- <xsl:with-param name="current-number" select="$current-number + 1"/>
- <xsl:with-param name="max-number" select="$max-number"/>
- </xsl:call-template>
- </xsl:if>
- </xsl:template>
* This source code was highlighted with Source Code Highlighter.
Здесь условие в xsl:if, как многие уже догадались, предназначено для выхода из рекурсии.
Вызвав шаблон с помощью кода (входным XML можно указать любой валидный XML файл)
- <xsl:template match="/">
- <xsl:call-template name="numbers">
- <xsl:with-param name="current-number" select="1"/>
- <xsl:with-param name="max-number" select="50"/>
- </xsl:call-template>
- </xsl:template>
* This source code was highlighted with Source Code Highlighter.
получим вывод всех чисел от 1 до 50 через пробел.
Тот, кто понял основную идею, сразу поймет, где это можно и нужно применять и может пропустить следующую часть статьи.
Итак, реальные примеры.
Пример №1
Имеем XML:
- <?xml version="1.0"?>
- <strings>
- <string>bla1 bla2 bla1 bla2 bla1</string>
- </strings>
* This source code was highlighted with Source Code Highlighter.
Необходимо разбить строку внутри ноды string по пробелам и каждый нечетный элемент вывести зеленым цветом, а каждый четный элемент — красным.
В XSLT/XPATH 2.0 имеется замечательная функция tokenize, которая может разбить строку на части и, соответственно, с помощью xsl:for-each мы можем пробежаться по ней и сделать с каждой частью, все что захотим. Но XSLT 2.0, насколько мне известно, хорошо поддерживается только процессором Saxon. Встроенные XSLT процессоры браузеров и libxslt в PHP его поддержки не имеют, поэтому будем применять рекурсивный шаблон.
- <?xml version='1.0'?>
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
-
- <!-- можно матчить и просто string,
- тогда содержимое ноды будет преобразовано к тексту,
- но, если внутри string будут еще ноды (например, <br />),
- можем получить ошибку
- не забываем, что если собираемся работать с текстом,
- то нужно и матчить именно текст,
- чтобы работа вашего кода была очевидной -->
- <xsl:template match="string/text()">
- <xsl:call-template name="colorer">
- <xsl:with-param name="text" select="." />
- <!-- остальные значения возьмутся из дефолтных значений -->
- </xsl:call-template>
- </xsl:template>
-
- <xsl:template name="colorer">
- <xsl:param name="text" />
- <!-- дефолтный разделитель - пробел -->
- <xsl:param name="delimeter" select="' '" />
- <!-- по дефолту элемент раскрашивается, как нечетный -->
- <xsl:param name="even" select="false" />
- <xsl:variable name="color">
- <xsl:choose>
- <xsl:when test="$even">
- <xsl:text>red</xsl:text>
- </xsl:when>
- <xsl:otherwise>
- <xsl:text>green</xsl:text>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
- <xsl:choose>
- <!-- если строка содержит разделитель -->
- <xsl:when test="contains($text, $delimeter)">
- <!-- то выводим строку до разделителя -->
- <span class="{$color}"><xsl:value-of select="substring-before($text, $delimeter)" /></span>
- <!-- и еще раз вызываем шаблон для оставшейся строки -->
- <xsl:call-template name="colorer">
- <xsl:with-param name="delimeter" select="$delimeter" />
- <xsl:with-param name="even" select="not($even)" />
- <xsl:with-param name="text" select="substring-after($text, $delimeter)" />
- </xsl:call-template>
- </xsl:when>
- <xsl:otherwise>
- <span class="{$color}"><xsl:value-of select="$text" /></span>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:template>
-
- </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.
Этот пример надуманный, вряд ли кому-то понадобится такая реализация, но очень похожие задачи встречаются нередко, поэтому решил показать именно на таком примере.
Пример №2
Это уже реально рабочий пример, который может кому-то понадобиться в работе, думаю, что с небольшими переделками каждый сможет использовать его в своих проектах.
Он получился довольно большой, потому что здесь не только используются рекурсивные шаблоны, но и наводятся всякие красивости, напрямую не относящиеся к рекурсивным шаблонам, но делающие результат наиболее приближенным к боевым условиям.
Итак, имеем XML вида:
- <?xml version="1.0"?>
- <forum>
- <pages>
- <current-page number="3" />
- <topics-per-page count="15" />
- <topics count="150" />
- <link href="page.php?number=" />
- </pages>
- <themes>
- <theme>theme1</theme>
- <theme>theme2</theme>
- <theme>theme3</theme>
- <theme>theme4</theme>
- <theme>theme5</theme>
- <theme>theme6</theme>
- <theme>theme7</theme>
- <theme>theme8</theme>
- <theme>theme9</theme>
- <theme>theme10</theme>
- <theme>theme11</theme>
- <theme>theme12</theme>
- <theme>theme13</theme>
- <theme>theme14</theme>
- <theme>theme15</theme>
- </themes>
- </forum>
* This source code was highlighted with Source Code Highlighter.
Необходимо вывести номера страниц (pager). Перед и после номеров страниц нужно вывести переход на предыдущую и следующую страницу при условии, что мы находимся не на первой или не на последней странице. Также ставим условие, что необходимо вывести только ссылки для перехода на несколько ближайших страниц, а не на все сразу, поскольку тогда pager может получиться очень большим.
Получаем вот такой довольно большой шаблон:
- <?xml version="1.0"?>
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
-
- <xsl:template match="pages">
- <xsl:call-template name="page-numbers">
- <xsl:with-param name="total-results" select="topics/@count"/>
- <xsl:with-param name="results-per-page" select="topics-per-page/@count"/>
- <xsl:with-param name="max-from-current-page" select="3"/>
- <xsl:with-param name="current-page" select="current-page/@number"/>
- <xsl:with-param name="href" select="link/@href"/>
- </xsl:call-template>
- </xsl:template>
-
- <xsl:template name="page-numbers">
- <xsl:param name="total-results"/>
- <xsl:param name="results-per-page"/>
- <xsl:param name="max-from-current-page"/>
- <xsl:param name="current-page"/>
- <xsl:param name="href"/>
-
-
- <!-- Сколько всего страниц имеем -->
- <xsl:variable name="max-page" select="ceiling($total-results div $results-per-page)"/>
-
- <!-- Если страниц больше одной, то выводим номера страниц -->
- <xsl:if test="1 < $max-page">
-
- <!-- С какой страницы начинать вывод номеров страниц -->
- <xsl:variable name="from-page">
- <xsl:choose>
- <!-- Если номер текущей страницы больше, чем максимальная удаленность -->
- <xsl:when test="$current-page > $max-from-current-page">
- <!-- То первой будет страница, удаленная на заданное число страниц от текущей -->
- <xsl:value-of select="$current-page - $max-from-current-page"/>
- </xsl:when>
- <xsl:otherwise>1</xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
-
- <!-- Какой страницей заканчивать вывод номеров страниц -->
- <xsl:variable name="to-page">
- <xsl:choose>
- <!-- Если номер текущей страницы удален от номера последней страницы больше, чем максимальная удаленность -->
- <xsl:when test="$max-page - $current-page > $max-from-current-page">
- <!-- То последней будет страница, удаленная на заданное число страниц от текущей -->
- <xsl:value-of select="$current-page + $max-from-current-page"/>
- </xsl:when>
- <xsl:otherwise>
- <xsl:value-of select="$max-page"/>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
-
- <!-- Если текушая страница не первая, то выводим стрелки со ссылкой на предыдущую страницу -->
- <xsl:if test="1 != $current-page">
- <a href="{$href}{$current-page - 1}"><<</a>
- <xsl:text> </xsl:text>
- </xsl:if>
-
- <!-- Вызываем шаблон номеров страниц с начальными значениями -->
- <xsl:call-template name="page-number">
- <xsl:with-param name="max-page-number" select="$to-page"/>
- <xsl:with-param name="current-number" select="$from-page"/>
- <xsl:with-param name="current-page" select="$current-page"/>
- <xsl:with-param name="href" select="$href"/>
- </xsl:call-template>
-
- <!-- Если текушая страница не последняя, то выводим стрелки со ссылкой на следущую страницу -->
- <xsl:if test="$max-page != $current-page">
- <xsl:text> </xsl:text>
- <a href="{$href}{$current-page + 1}">>></a>
- </xsl:if>
- </xsl:if>
- </xsl:template>
-
- <xsl:template name="page-number">
- <xsl:param name="max-page-number"/>
- <xsl:param name="current-number"/>
- <xsl:param name="current-page"/>
- <xsl:param name="href"/>
-
- <xsl:choose>
- <!-- Если выводим номер текущей страницы, то без ссылки -->
- <xsl:when test="$current-number = $current-page">
- <xsl:value-of select="$current-number"/>
- </xsl:when>
- <!-- Номера остальных страниц со ссылкой -->
- <xsl:otherwise>
- <a href="{$href}{$current-number}">
- <xsl:value-of select="$current-number"/>
- </a>
- </xsl:otherwise>
- </xsl:choose>
-
- <!-- Если текущий номер не последний, то вызываем шаблон вывода следующего номера -->
- <xsl:if test="$current-number < $max-page-number">
- <xsl:text> | </xsl:text>
- <xsl:call-template name="page-number">
- <xsl:with-param name="max-page-number" select="$max-page-number"/>
- <xsl:with-param name="current-number" select="$current-number + 1"/>
- <xsl:with-param name="current-page" select="$current-page"/>
- <xsl:with-param name="href" select="$href"/>
- </xsl:call-template>
- </xsl:if>
- </xsl:template>
-
- <xsl:template match="@* | node()">
- <xsl:copy>
- <xsl:apply-templates select="@* | node()" />
- </xsl:copy>
- </xsl:template>
- </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.
В принципе, по комментариям в коде, все должно довольно легко пониматься, но если есть вопросы, с удовольствием отвечу на них.
Полезное чтиво: Алексей Валиков — Технология XSLT — русская библия XSLT :)
В будущем хочу написать об использовании ключей и режимов в XSLT, а также раскрыть кучу типичных мелких ошибок начинающих. Также попробую написать о производительности XSLT преобразований, но вряд ли это будет серьезная статья с кучей статистических выкладок, скорее всего просто рассмотрим узкие места производительности. Не могу дать никаких гарантий, насколько быстро появятся эти статьи, но постараюсь особо не тянуть с ними.