В XSLT есть циклы: пагинатор (постраничная навигация), часть 1

от автора

Эта статья обязана появиться 1 апреля, потому что в ней рассказывается, как в XSLT реализованы циклы.

Время от времени всплывает умирающая технология XSLT (в просторечии ЧЫДЕ) и задаёт непростые вопросы. Как, например, взять максимум от 2 чисел в выражении или как организовать цикл. Соединением многих таких вопросов служит пагинатор — вывод навигации по нескольким страницам и, по возможности, удобный. На Javascript есть много примеров простых и удобных пагинаторов. Но если страницы с сервера выдаются в XML, то возникает крамольная мысль: почему бы всё оформление страниц, включая пагинатор, не сделать на статике, в XSLT? Ничего, что в эту статику можно включить JS и сделать всё проще. Кошерный подход лёгких путей не ищет.

Плюсы и минусы XSLT

Когда школьникам Бандиагары показали, в скольких строчках можно организовать цикл на XSLT, они реагировали примерно так:


То же было и со специалистами: habrastorage.org/storage2/451/896/6da/4518966da9d6f146434889bcfadb3ee8.jpg.

Вместо одной строчки на 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 — место, благодаря которому он прекращается.

&lt; — необходимость писать так некоторые символы (<, >, &, /) по особенностям языка.

Если число страниц переменное и задаётся числом в элементе , достаточно написать

<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. Строки, относящиеся непосредственно к пагинатору:

код XSLT

<?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+ и остальных современных браузерах.

ссылка на оригинал статьи http://habrahabr.ru/post/174977/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *