Pull to refresh

Рекурсивные шаблоны в XSLT

Reading time 12 min
Views 12K
Привет всем!
Хочу рассказать о применении рекурсивных шаблонов в XSLT, так как многие начинающие работать с XSLT встречаются с задачами, которые требуют их применения и не знают, как такие задачи решать.
Возьмем пару обычных примеров:
1. Имеется нода со строкой, ее необходимо разбить на части по определенному символу (в нашем случае возьмем символ пробела) и каждую часть разукрасить в разные цвета.
2. Сделаем вывод номеров страниц (pager) исходя из того, что нам известно общее количество объектов (например, тем форума), количество объектов на странице и номер страницы, на которой мы в данный момент находимся.

Для начала приведу пример-основу, где мы выведем несколько чисел с использованием рекурсивных шаблонов:

  1. <xsl:template name="numbers">
  2.   <xsl:param name="current-number"/>
  3.   <xsl:param name="max-number"/>
  4.   <xsl:value-of select="$current-number"/>
  5.   <!-- если не достигли последнего числа -->
  6.   <xsl:if test="$current-number < $max-number">
  7.     <!-- то выводим пробел после числа,
  8.     поскольку необходимо будет вывести еще числа -->
  9.     <xsl:text> </xsl:text>
  10.     <!-- и вызываем самого себя для вывода следующего числа -->
  11.     <xsl:call-template name="numbers">
  12.       <xsl:with-param name="current-number" select="$current-number + 1"/>
  13.       <xsl:with-param name="max-number" select="$max-number"/>
  14.     </xsl:call-template>
  15.   </xsl:if>
  16. </xsl:template>
* This source code was highlighted with Source Code Highlighter.


Здесь условие в xsl:if, как многие уже догадались, предназначено для выхода из рекурсии.

Вызвав шаблон с помощью кода (входным XML можно указать любой валидный XML файл)
  1. <xsl:template match="/">
  2.   <xsl:call-template name="numbers">
  3.     <xsl:with-param name="current-number" select="1"/>
  4.     <xsl:with-param name="max-number" select="50"/>
  5.   </xsl:call-template>
  6. </xsl:template>
* This source code was highlighted with Source Code Highlighter.

получим вывод всех чисел от 1 до 50 через пробел.

Тот, кто понял основную идею, сразу поймет, где это можно и нужно применять и может пропустить следующую часть статьи.

Итак, реальные примеры.

Пример №1


Имеем XML:
  1. <?xml version="1.0"?>
  2. <strings>
  3.   <string>bla1 bla2 bla1 bla2 bla1</string>
  4. </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 его поддержки не имеют, поэтому будем применять рекурсивный шаблон.
  1. <?xml version='1.0'?>
  2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3.  
  4. <!-- можно матчить и просто string,
  5. тогда содержимое ноды будет преобразовано к тексту,
  6. но, если внутри string будут еще ноды (например, <br />),
  7. можем получить ошибку
  8. не забываем, что если собираемся работать с текстом,
  9. то нужно и матчить именно текст,
  10. чтобы работа вашего кода была очевидной -->
  11. <xsl:template match="string/text()">
  12.   <xsl:call-template name="colorer">
  13.     <xsl:with-param name="text" select="." />
  14.     <!-- остальные значения возьмутся из дефолтных значений -->
  15.   </xsl:call-template>
  16. </xsl:template>
  17.  
  18. <xsl:template name="colorer">
  19.   <xsl:param name="text" />
  20.   <!-- дефолтный разделитель - пробел -->
  21.   <xsl:param name="delimeter" select="' '" />
  22.   <!-- по дефолту элемент раскрашивается, как нечетный -->
  23.   <xsl:param name="even" select="false" />
  24.   <xsl:variable name="color">
  25.     <xsl:choose>
  26.       <xsl:when test="$even">
  27.         <xsl:text>red</xsl:text>
  28.       </xsl:when>
  29.       <xsl:otherwise>
  30.         <xsl:text>green</xsl:text>
  31.       </xsl:otherwise>
  32.     </xsl:choose>
  33.   </xsl:variable>
  34.   <xsl:choose>
  35.     <!-- если строка содержит разделитель -->
  36.     <xsl:when test="contains($text, $delimeter)">
  37.       <!-- то выводим строку до разделителя -->
  38.       <span class="{$color}"><xsl:value-of select="substring-before($text, $delimeter)" /></span>
  39.       <!-- и еще раз вызываем шаблон для оставшейся строки -->
  40.       <xsl:call-template name="colorer">
  41.         <xsl:with-param name="delimeter" select="$delimeter" />
  42.         <xsl:with-param name="even" select="not($even)" />
  43.         <xsl:with-param name="text" select="substring-after($text, $delimeter)" />
  44.       </xsl:call-template>
  45.     </xsl:when>
  46.     <xsl:otherwise>
  47.       <span class="{$color}"><xsl:value-of select="$text" /></span>
  48.     </xsl:otherwise>
  49.   </xsl:choose>
  50. </xsl:template>
  51.  
  52. </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.

Этот пример надуманный, вряд ли кому-то понадобится такая реализация, но очень похожие задачи встречаются нередко, поэтому решил показать именно на таком примере.

Пример №2


Это уже реально рабочий пример, который может кому-то понадобиться в работе, думаю, что с небольшими переделками каждый сможет использовать его в своих проектах.
Он получился довольно большой, потому что здесь не только используются рекурсивные шаблоны, но и наводятся всякие красивости, напрямую не относящиеся к рекурсивным шаблонам, но делающие результат наиболее приближенным к боевым условиям.
Итак, имеем XML вида:
  1. <?xml version="1.0"?>
  2. <forum>
  3.   <pages>
  4.     <current-page number="3" />
  5.     <topics-per-page count="15" />
  6.     <topics count="150" />
  7.     <link href="page.php?number=" />
  8.   </pages>
  9.   <themes>
  10.     <theme>theme1</theme>
  11.     <theme>theme2</theme>
  12.     <theme>theme3</theme>
  13.     <theme>theme4</theme>
  14.     <theme>theme5</theme>
  15.     <theme>theme6</theme>
  16.     <theme>theme7</theme>
  17.     <theme>theme8</theme>
  18.     <theme>theme9</theme>
  19.     <theme>theme10</theme>
  20.     <theme>theme11</theme>
  21.     <theme>theme12</theme>
  22.     <theme>theme13</theme>
  23.     <theme>theme14</theme>
  24.     <theme>theme15</theme>
  25.   </themes>
  26. </forum>
* This source code was highlighted with Source Code Highlighter.

Необходимо вывести номера страниц (pager). Перед и после номеров страниц нужно вывести переход на предыдущую и следующую страницу при условии, что мы находимся не на первой или не на последней странице. Также ставим условие, что необходимо вывести только ссылки для перехода на несколько ближайших страниц, а не на все сразу, поскольку тогда pager может получиться очень большим.
Получаем вот такой довольно большой шаблон:
  1. <?xml version="1.0"?>
  2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  3.  
  4.   <xsl:template match="pages">
  5.     <xsl:call-template name="page-numbers">
  6.       <xsl:with-param name="total-results" select="topics/@count"/>
  7.       <xsl:with-param name="results-per-page" select="topics-per-page/@count"/>
  8.       <xsl:with-param name="max-from-current-page" select="3"/>
  9.       <xsl:with-param name="current-page" select="current-page/@number"/>
  10.       <xsl:with-param name="href" select="link/@href"/>
  11.     </xsl:call-template>
  12.   </xsl:template>
  13.  
  14.   <xsl:template name="page-numbers">
  15.     <xsl:param name="total-results"/>
  16.     <xsl:param name="results-per-page"/>
  17.     <xsl:param name="max-from-current-page"/>
  18.     <xsl:param name="current-page"/>
  19.     <xsl:param name="href"/>
  20.  
  21.  
  22.     <!-- Сколько всего страниц имеем -->
  23.     <xsl:variable name="max-page" select="ceiling($total-results div $results-per-page)"/>
  24.  
  25.     <!-- Если страниц больше одной, то выводим номера страниц -->
  26.     <xsl:if test="1 < $max-page">
  27.  
  28.       <!-- С какой страницы начинать вывод номеров страниц -->
  29.       <xsl:variable name="from-page">
  30.         <xsl:choose>
  31.           <!-- Если номер текущей страницы больше, чем максимальная удаленность -->
  32.           <xsl:when test="$current-page > $max-from-current-page">
  33.             <!-- То первой будет страница, удаленная на заданное число страниц от текущей -->
  34.             <xsl:value-of select="$current-page - $max-from-current-page"/>
  35.           </xsl:when>
  36.           <xsl:otherwise>1</xsl:otherwise>
  37.         </xsl:choose>
  38.       </xsl:variable>
  39.  
  40.       <!-- Какой страницей заканчивать вывод номеров страниц -->
  41.       <xsl:variable name="to-page">
  42.         <xsl:choose>
  43.           <!-- Если номер текущей страницы удален от номера последней страницы больше, чем максимальная удаленность -->
  44.           <xsl:when test="$max-page - $current-page > $max-from-current-page">
  45.             <!-- То последней будет страница, удаленная на заданное число страниц от текущей -->
  46.             <xsl:value-of select="$current-page + $max-from-current-page"/>
  47.           </xsl:when>
  48.           <xsl:otherwise>
  49.             <xsl:value-of select="$max-page"/>
  50.           </xsl:otherwise>
  51.         </xsl:choose>
  52.       </xsl:variable>
  53.  
  54.       <!-- Если текушая страница не первая, то выводим стрелки со ссылкой на предыдущую страницу -->
  55.       <xsl:if test="1 != $current-page">
  56.         <a href="{$href}{$current-page - 1}"><<</a>
  57.         <xsl:text> </xsl:text>
  58.       </xsl:if>
  59.  
  60.       <!-- Вызываем шаблон номеров страниц с начальными значениями -->
  61.       <xsl:call-template name="page-number">
  62.         <xsl:with-param name="max-page-number" select="$to-page"/>
  63.         <xsl:with-param name="current-number" select="$from-page"/>
  64.         <xsl:with-param name="current-page" select="$current-page"/>
  65.         <xsl:with-param name="href" select="$href"/>
  66.       </xsl:call-template>
  67.  
  68.       <!-- Если текушая страница не последняя, то выводим стрелки со ссылкой на следущую страницу -->
  69.       <xsl:if test="$max-page != $current-page">
  70.         <xsl:text> </xsl:text>
  71.         <a href="{$href}{$current-page + 1}">>></a>
  72.       </xsl:if>
  73.     </xsl:if>
  74.   </xsl:template>
  75.  
  76.   <xsl:template name="page-number">
  77.     <xsl:param name="max-page-number"/>
  78.     <xsl:param name="current-number"/>
  79.     <xsl:param name="current-page"/>
  80.     <xsl:param name="href"/>
  81.  
  82.     <xsl:choose>
  83.       <!-- Если выводим номер текущей страницы, то без ссылки -->
  84.       <xsl:when test="$current-number = $current-page">
  85.         <xsl:value-of select="$current-number"/>
  86.       </xsl:when>
  87.       <!-- Номера остальных страниц со ссылкой -->
  88.       <xsl:otherwise>
  89.         <a href="{$href}{$current-number}">
  90.           <xsl:value-of select="$current-number"/>
  91.         </a>
  92.       </xsl:otherwise>
  93.     </xsl:choose>
  94.  
  95.     <!-- Если текущий номер не последний, то вызываем шаблон вывода следующего номера -->
  96.     <xsl:if test="$current-number < $max-page-number">
  97.       <xsl:text> | </xsl:text>
  98.       <xsl:call-template name="page-number">
  99.         <xsl:with-param name="max-page-number" select="$max-page-number"/>
  100.         <xsl:with-param name="current-number" select="$current-number + 1"/>
  101.         <xsl:with-param name="current-page" select="$current-page"/>
  102.         <xsl:with-param name="href" select="$href"/>
  103.       </xsl:call-template>
  104.     </xsl:if>
  105.   </xsl:template>
  106.  
  107.   <xsl:template match="@* | node()">
  108.     <xsl:copy>
  109.       <xsl:apply-templates select="@* | node()" />
  110.     </xsl:copy>
  111.   </xsl:template>
  112. </xsl:stylesheet>
* This source code was highlighted with Source Code Highlighter.

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

Полезное чтиво: Алексей Валиков — Технология XSLT — русская библия XSLT :)

В будущем хочу написать об использовании ключей и режимов в XSLT, а также раскрыть кучу типичных мелких ошибок начинающих. Также попробую написать о производительности XSLT преобразований, но вряд ли это будет серьезная статья с кучей статистических выкладок, скорее всего просто рассмотрим узкие места производительности. Не могу дать никаких гарантий, насколько быстро появятся эти статьи, но постараюсь особо не тянуть с ними.
Tags:
Hubs:
+57
Comments 59
Comments Comments 59

Articles