Время от времени всплывает умирающая технология XSLT и задаёт непростые вопросы. Как, например, взять максимум от 2 чисел в выражении или как организовать цикл. Соединением многих таких вопросов служит пагинатор — вывод навигации по нескольким страницам и, по возможности, удобный. На Javascript есть много примеров простых и удобных пагинаторов. Но если страницы с сервера выдаются в XML, то возникает крамольная мысль: почему бы всё оформление страниц, включая пагинатор, не сделать на статике, в XSLT? Ничего, что в эту статику можно включить JS и сделать всё проще. Кошерный подход лёгких путей не ищет.
Плюсы и минусы XSLT
Вместо одной строчки на JS нужно писать десяток строчек рекурсии с рядом особенностей. Пусть это минус. Тем не менее, задача эта выполнима, значит, когда-то где-то всплывёт такое решение.
В Сети очень ценятся ответы, похожие на вопросы о том, как сложить 2 числа: "Как комментировать XSLT так, чтобы комменты из HTML не удалялись". Значит, при наличии минимальных знаний репутация на SO вам будет обеспечена.
За годы существования XSLT многие браузеры, кроме очень старых и простых, наработали умение обрабатывать XML+XSLT. Этим можно пользоваться, перекладывая работу с сервера на клиентов (браузеры), пусть даже лишними сотнями строк трудно понимаемого декларативного кода.
Часто декларативность — это хорошо. Набор правил, как в CSS, легче понимается и используется. Но начинать писать рекурсивные процедуры на декларативном языке с родовыми проблемами синтаксиса — это занятие для гиков и людей, попавших в безысходную ситуацию. Поэтому целей у статьи две — почитать и поиграться на досуге — для гиков, а взять работающий пример и настроить его под себя — для людей.
На Хабре подобная тема поднималась здесь: habrahabr.ru/post/138740/ (реализация пагинатора).
Что нужно от пагинатора
От XML он получает только номер текущей страницы и (возможно) номер последней страницы списка. Всё остальное настраивается в статике пагинатора в *.xsl. «Остального» немного, как будет видно из постановки. Всего лишь число ссылок вокруг ссылки на текущую страницу. Но потом добавилось расширение — вывод следующих страниц через десятки или другой интервал. Это показалось хорошей иллюстрацией возможностей пагинатора.
1) выводить текущую страницу (со ссылкой, если требуется иногда обновление её самой или без ссылки);
2) выводить несколько ссылок соседних страниц вокруг (до и после текущей);
3) первую и последнюю (крайние) страницы, если они не попали в «соседние»;
4) троеточие, если между соседними и крайней есть непоказанные страницы;
5) опционально — ссылки на троеточиях, чтобы перейти примерно на середину непоказываемого промежутка страниц;
6) если часть ссылок не выведена, потому что встретились края, добавить невыведенное количество ссылок с другой стороны ссылки текущей страницы. Другими словами — показывать, если есть, что показывать в пределах заданного количества ссылок. Например, показываем 5 ссылок «до» и 5 «после», но при просмотре третьей страницы отображается 2 ссылки «до». Значит, показать 8 ссылок «после», если такие найдутся (не выйдут за пределы максимального числа страниц).
(Это требование выполнено частично — выводятся лишние ссылки справа, когда номер страницы — возле первой и выводится только половина списка, если номер страницы близок к максимальному. Это связано с тем, что потребовало бы большой переделки логики и усложнения выражений, а цели такой строго не стояло.)
7) наконец, двойное использование функции пагинатора — вывод страниц через десятки (или пятёрки, всё настраивается) вслед за первыми. Может быть полезно, если надо быстро перейти вглубь очень большого списка, на десятки страниц, а находимся обычно на первых страницах. Если указана концевая страница, список десятков не выводится.
Окунёмся в дао XSLT
Чтобы рассказ оказался полезным, построим его в виде обучения приёмам программирования на этом декларативном языке. Будем строить пагинатор, от простых моделей до всё более сложных.
За основу построения возьмём некоторый файл логов, которые часто встречаются у веб-администраторов и которые приходится просматривать. Чтобы просматривание было удобным, а затраты на программирование — небольшие, выдаём логи постранично в XML, а всё оформление возлагается на клиентские технологии, включая клиентский XSLT.
Как упоминалось, пагинатор естественнее делать на процедурном языке. Но и XSLT справляется с этой задачей, выполняет всё требования постановки. В интернете разбросано множество примеров реализации и даже один встретился на Хабре. Но примеры без пояснений правил построения приводят к тому, что реализацию приходится делать самостоятельно, начиная с основ. Данный пример — попытка дать пример законченного и функционального пагинатора, для которого есть надежда, что подключение будет простым, а управление им — задокументированным.
Пока записей в нашем логе порядка 500, самый простой способ пагинации — просто вывести 10 ссылок на странице и вручную записать им номера страниц на HTML, вида:
<a href="page.xml?page=2"/>2</a>
Если их немного больше 500 или глубже записи смотрятся редко, достаточно приписать формочку ввода номера страницы. Тоже выход. Это не потребует углубления в XSLT и делается в xsl-файле на общих основаниях.
<?xml version="1.0"?>
<!DOCTYPE html>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<head><title>Someone Log</title>
<meta http-equiv="x-ua-compatible" content="IE=8"/>
<style>
body{
..../* стили для страницы, скрипты, в которых заменены некоторые символы на сущности */
}
</style>
<script type="text/javascript">
...
</script>
</head>
<body> <!-- выводим таблицу с данными, ячейку за ячейкой, ничего интересного -->
<table class="tb1" id="tb1">
<tr>
<th>ip + <span class="n">#</span></th>
<th>path</th>
<th>browser</th>
<th>accType</th>
<th>fileName</th>
<th>settings</th>
<th>date</th>
</tr>
<xsl:for-each select="/ha/actions/action">
<tr class="account-{accountType} {fileName}">
<td class="help leftJust" title2="{@id}">
<div class="full">
<span><xsl:value-of select="@id"/></span>
</div>
<div class="brief"><xsl:value-of select="ip"/></div>
</td>
<td class="leftJust"><a href="http://habrahabr.ru{path}" target="_blank">
<xsl:value-of select="path"/>
</a></td>
<td class="help UA" title2="{agent}" align="center">
<div class="full">
<div class="fullRel">
<span><xsl:value-of select="agent"/></span>
</div>
</div>
<div class="brief"><xsl:value-of select="browser"/></div>
</td>
<td><xsl:value-of select="accountType"/></td>
<td class="fileName {fileName}"><xsl:value-of select="fileName"/></td>
<td>
<span class="{settings/property/@value}">
<xsl:value-of select="settings/property/@name"/>
</span>
</td>
<xsl:variable name="dt" select="date"/>
<td><span title="{substring($dt,1,10)}">
<xsl:value-of select="substring($dt,12,10)"/>
</span></td>
</tr>
</xsl:for-each>
</table>
<div class="pagination"> <!-- начался блок пагинации -->
<!-- В первом приближении хватает просто HTML: -->
<span class="">
<a href="page.xml?page=1"/>1</a>
</span>
<span class="">
<a href="page.xml?page=2"/>2</a>
</span>
<span class="">
<a href="page.xml?page=3"/>3</a>
</span>
...<!-- и так - 10 ссылок -->
</body>
</html>
Первая же незадача — затруднительно даже написать класс текущей страницы, чтобы как-то её выделить или дезактивировать. Нет проблем, есть JS для этого. Но в планах — написание ссылок на XSLT. Поэтому нехотя, но посмотрим, как пишут этот странный цикл на 10 строчек и организуем для начала вывод списка ссылок на XSLT.
Создаём рекурсивную функцию. Из тела шаблона вызывают шаблон-функцию. Все папраметры для неё надо передавать — это независимые пространства имён, поэтому нельзя, как в обычных языках, определить глобальные области видимости.
<xsl:template match="/">
...
<xsl:comment>====== в тексте страницы, вместо 10 ссылок ======</xsl:comment>
<xsl:call-template name="paginate">
<xsl:with-param name="nLinks" select="10"/>
<xsl:with-param name="p" select="/ha/page"/>
<xsl:with-param name="url" select="$url"/>
</xsl:call-template>
</div></body></html>
</xsl:template>
<xsl:comment>====== функция-цикл - исполнение с концевой рекурсией ======</xsl:comment>
<xsl:template name="paginate">
<xsl:param name="i" select="1"/> <xsl:comment>параметр (переменная) цикла</xsl:comment>
<xsl:param name="nLinks"/>
<xsl:param name="p"/>
<xsl:param name="url"/>
<xsl:if test="$i <= $nLinks">
<span class="{concat('active', number($i = $p)) }">
<a href="{concat($url, $i)}">
<xsl:value-of select="$i"/>
</a>
</span>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="$i + 1"/>
<xsl:with-param name="nLinks" select="$nLinks"/>
<xsl:with-param name="p" select="$p"/>
<xsl:with-param name="url" select="$url"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Ура, мы сделали цикл! 5 строчек вызова и 15 строчек функции сделали своё дело — мы можем отмечать текущую ссылку и не писать 30 строчек HTML! Это — достижение, первый шаг к покорению пагинации. И ничего, что на JS мы обошлись бы 5 и читалось бы лучше. Главное — привыкнуть, а дальше будет просветление.
Для укорочения кода на штук 8 строчек сделан трюк — для задания класса не записан блок choose-when-otherwise, а дописывается 1 или 0 к слову «active», таким образом, «active1» = класс ссылки текущей страницы.
На этом участке видны особенности языка: параметры, заданные по умолчанию, можно не задавать при вызове; в рекурсии обязательно перечисление всех нужных параметров. select="$i + 1" — ключевое место, благодаря которому двигается цикл, а test="$i <= $nLinks — место, благодаря которому он прекращается.
< — необходимость писать так некоторые символы (<, >, &, /) по особенностям языка.
Если число страниц переменное и задаётся числом в элементе , достаточно написать
<xsl:with-param name="nLinks" select="/ha/pageLast"/>
Симметричные ссылки «до» и «после»
Следующая задача: вывести ограниченное число ссылок, половина которых будет идти до ссылки текущей страницы, а вторая половина — после. Используем переменные для тех выражений, которые многократно повторяются. Прокручиваем цикл по интервалу, но не выводим ссылки для номеров, меньших 1. Для чётного количества ссылок считаем, что ссылок «до» будет на 1 больше (скорее всего, это число всегда будет задано нечётным, но протестировать надо для всех случаев).
Для реализации понадобился дополнительный параметр «to», в котором будет храниться максимальный номер страницы и передаваться по рекурсии.
<div class="pagination">
Страницы:
<xsl:variable name="url">http://37.230.115.43/actions/last.xml?page=</xsl:variable>
<xsl:variable name="p" select="/ha/page"/> <xsl:comment>текущая страница</xsl:comment>
<xsl:variable name="nL" select="9"/> <xsl:comment>сколько ссылок в пагинаторе</xsl:comment>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="$p"/>
<xsl:with-param name="nLinks" select="$nL"/>
<xsl:with-param name="url" select="$u"/>
</xsl:call-template>
</div></body></html>
</xsl:template>
<xsl:template name="paginate">
<xsl:param name="i" select="1"/>
<xsl:param name="nLinks"/>
<xsl:param name="url"/>
<xsl:param name="to" select="$i + $nLinks"/>
<xsl:variable name="n2" select="floor($nLinks div 2)"/>
<xsl:if test="$i < $to">
<xsl:if test="$i - $n2 >= 1">
<span class="{concat('active', number($i = $to - ceiling($nLinks div 2))) }">
<a href="{concat($url, $i - $n2)}">
<xsl:value-of select="$i - $n2"/>
</a>
</span>
</xsl:if>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="$i + 1"/>
<xsl:with-param name="url" select="$url"/>
<xsl:with-param name="nLinks" select="$nLinks"/>
<xsl:with-param name="to" select="$to"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Не хватает 2 вещей: ссылки на первую страницу, когда нужно, и вывода полного числа ссылок, а не половинного, когда находимся на первой странице. Добавляем перед вызовом функции проверки, когда надо выводить ссылку «1», а когда — троеточие, означающее пропуск части ссылок страниц.
В функцию добавляем счётчик, который считает, сколько в реальности добавлено ссылок, чтобы остановить цикл по достижению $nLinks, а не как сейчас, по количеству $nLinks.
Решение со счётчиком — простое. Этим решением закладывается пара логических бомб, решать которые придётся позже.
1) цикл может никогда не закончиться; ну, это просто, введём ещё контрольный счётчик с числом, скажем, 50, на всякий случай; хм, уже 2 счётчика. Решение не такое красивое, как казалось;
2) начало страниц легко просчитаем, а вот вблизи конца списка страниц — понадобится предугадывать, сколько номеров зайдёт за край допустимого и не будет показано. Но не всё сразу.
Так отмечается (условно) первая страница.
<xsl:variable name="pn2" select="$p - floor($nL div 2)"/>
<xsl:if test="$pn2 > 1">
<span class="">
<a href="{concat($url, 1)}">1</a>
<xsl:if test="$pn2 > 2">
<a class="ellip" title="{floor(($pn2 +1) div 2)}" href="{concat($url, floor(($pn2 +1) div 2) )}">...</a>
</xsl:if>
</span>
</xsl:if>
На ссылке на троеточии выведена примерно срединная ссылка непоказанного интервала, указываемая в подсказке. Например, показ ссылок начинается с 60-й страницы — 30-я или 29-я будет создана на троеточии. Ссылка без показа числа — лаконичнее, полезнее и совершенно не требует дополнительного места. Троеточия не выводятся, если ссылки показываются, начиная со второй.
Защиты
От разработчика, который начнёт что-то менять в параметрах и случайно задаст, например, минус миллион — ограничиваем число рекурсий, введя параметр stop, равный 50. С ним пагинатор не совершит более 50 итераций.
Пагинация через интервал (пункт 7)
Когда каркас написан, остальные «фичи» добавляются легко (конечно, если разработчик уже в курсе технологий). Чтобы это продемонстрировать, в конечный пагинатор добавим возможность вывода ссылок с интервалом в несколько страниц. иногда это нужно для навигации, иногда — для счёта не страниц, а записей на страницах. Это будет немного нецелевое использование пагинатора, потому что он настроен на вывод ссылок «до и после», а для вывода через интервал это проявится. Но вместо того, чтобы писать новый пагинатор или корректировать этот на нецелевое использование, проще правильно подобрать начальный параметр его, а именно — прибавить floor($n2 div 2). С этой оговоркой и с добавленным параметром step пагинатор начинает работать.
Продолжение следует, но если читатель пожелает посмотреть и использовать готовый пагинатор, он лежит по адресу spmbt.kodingen.com/wk/37.20.115.43.xml. Адреса и ссылки на странице лога изменены, совпадения случайны. Переключатель по страницам деактивирован, поскольку это — статический пример, всегда находящийся на 9-й странице. Но вверху видим пагинатор, построенный через spmbt.kodingen.com/wk/37.20.115.43.xsl. Строки, относящиеся непосредственно к пагинатору:
<?xml version="1.0"?>
<!DOCTYPE html>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
...
<div class="pagination">
Страницы:
<xsl:variable name="url">#page=</xsl:variable>
<xsl:variable name="p" select="/ha/page"/> <xsl:comment>текущая страница</xsl:comment>
<xsl:variable name="nL" select="11"/> <xsl:comment>сколько ссылок в пагинаторе</xsl:comment>
<xsl:variable name="pLast" select="/ha/pageLast"/> <xsl:comment>последняя (если есть; а если нет, то здесь будет пустая строка)</xsl:comment>
<xsl:variable name="pn2" select="$p - floor($nL div 2)"/>
<xsl:if test="$pn2 > 1">
<span class="">
<a href="{concat($url, 1)}">1</a>
<xsl:if test="$pn2 > 2">
<a class="ellip" title="{floor(($pn2 +1) div 2)}" href="{concat($url, floor(($pn2 +1) div 2) )}">...</a>
</xsl:if>
</span>
</xsl:if>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="$p"/>
<xsl:with-param name="nLinks" select="$nL"/>
<xsl:with-param name="pLast" select="$pLast"/>
<xsl:with-param name="url" select="$url"/>
</xsl:call-template>
<xsl:if test="string-length($pLast) =0">
<xsl:variable name="nL2" select="5"/>
<xsl:variable name="step" select="10"/>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="floor(($p + $nL + $step +1) div $step) * $step + floor($nL2 div 2)"/>
<xsl:with-param name="nLinks" select="$nL2"/>
<xsl:with-param name="pLast" select="$pLast"/>
<xsl:with-param name="step" select="$step"/>
<xsl:with-param name="url" select="$url"/>
<xsl:with-param name="class" select="'gaps'"/>
</xsl:call-template>
</xsl:if>
<xsl:variable name="pp2" select="$p + floor(($nL -1) div 2)"/>
<xsl:if test="$pp2 < $pLast">
<span class="">
<xsl:if test="$pp2 < $pLast -1">
<a class="ellip" title="{$pLast - floor(($pLast - $pp2) div 2)}" href="{concat($url, $pLast - floor(($pLast - $pp2) div 2) )}">...</a>
</xsl:if>
<a href="{concat($url, $pLast)}"><xsl:value-of select="$pLast"/></a>
</span>
</xsl:if>
</div>
</body>
</html>
</xsl:template>
<xsl:template name="paginate">
<xsl:param name="i" select="1"/>
<xsl:param name="nLinks"/>
<xsl:param name="pLast"/>
<xsl:param name="step" select="1"/>
<xsl:param name="to" select="$i + $nLinks"/>
<xsl:param name="url"/>
<xsl:param name="class"/>
<xsl:param name="count" select="1"/>
<xsl:param name="stop" select="50"/>
<xsl:variable name="n2" select="floor($nLinks div 2)"/>
<xsl:if test="($i < $to or $count <= $nLinks) and $stop > 0">
<xsl:if test="$i - $n2 >= 1 and $i - $n2 <= $pLast or $i - $n2 >= 1 and string-length($pLast) =0">
<span class="{concat($class,' active', number($i = $to - ceiling($nLinks div 2)))}">
<a href="{concat($url, $i - $n2)}">
<xsl:value-of select="$i - $n2"/>
</a>
</span>
</xsl:if>
<xsl:call-template name="paginate">
<xsl:with-param name="i" select="$i + $step"/>
<xsl:with-param name="to" select="$to"/>
<xsl:with-param name="nLinks" select="$nLinks"/>
<xsl:with-param name="pLast" select="$pLast"/>
<xsl:with-param name="step" select="$step"/>
<xsl:with-param name="url" select="$url"/>
<xsl:with-param name="class" select="$class"/>
<xsl:with-param name="count" select="$count + number($i - $n2 >= 1 and $i - $n2 <= $pLast or $i - $n2 >= 1 and string-length($pLast) =0)"/>
<xsl:with-param name="stop" select="$stop - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
. На основной код ушло 85 строк — это хороший результат, при том, что выполняется 2 цикла — по страницам и по десяткам страниц, задействованы троеточия со срединными ссылками. Поддерживается в IE8+ и остальных современных браузерах.
Автор: spmbt