Александр Самиляк
28 июня 2011 |
|
Задача. | Разобрать по косточкам теорию и практику использования временных деревьев в XSLT. |
||
Если читатель писал низкоуровневый XSL (который решает общую задачу и выводит какой-то кастомизированный HTML-блок), то он наверняка сталкивался с необходимостью объявления множества параметров, с помощью которых и производится эта кастомизация. Вот, например, шаблон для вывода декорированного (скругленного) блока:
<xsl:template name="decor"> <xsl:param name="class" /> <xsl:param name="id" /> <xsl:param name="content" /> <div> <xsl:if test="$class"> <xsl:attribute name="class"> <xsl:value-of select="$class" /> </xsl:attribute> </xsl:if> <xsl:if test="$id"> <xsl:attribute name="id"> <xsl:value-of select="$id" /> </xsl:attribute> </xsl:if> ... <xsl:copy-of select="$content" /> ... </div> </xsl:template>
Понятно, что тот, кто желает использовать наш шаблон в своем коде (назовем этот код клиентским по отношению к нашему шаблону), может захотеть добавить скругленному блоку какой-то класс, id, возможно, даже инлайновый стиль и вообще любой HTML-атрибут. Если мы хотим написать хороший низкий код, то надо закладывать максимум гибкости.
Поэтому, чтобы не ограничивать клиентский код рамками заранее определенных атрибутов, здесь хорошо бы принимать параметром набор пар «ключ — значение», который очень похож на объект в JavaScript. Ключ — название атрибута, значение — очевидно, значение атрибута. Временное дерево, по сути, и есть такой объект и отлично подходит для решения этой задачи:
<xsl:template name="decor"> <xsl:param name="attrs" /> <xsl:param name="content" /> <div> <xsl:for-each select="exsl:node-set($attrs)/*"> <xsl:if test="normalize-space(.)"> <xsl:attribute name="{name(.)}"> <xsl:value-of select="." /> </xsl:attribute> </xsl:if> </xsl:for-each> ... <xsl:copy-of select="$content" /> ... </div> </xsl:template> <xsl:call-template name="decor"> <xsl:with-param name="attrs"> <class>my_decor</class> <id>ER</id> <onclick>return { nominations: "124 Emmy" }</onclick> </xsl:with-param> <xsl:with-param name="content"> <p>ER is set in the emergency room of County General Hospital in Chicago.</p> </xsl:with-param> </xsl:call-template>
Пойдем дальше. Не знаю как вас, а меня жутко достало при выводе класса, пришедшего в мой шаблон с параметром, проверять, не пустой ли он:
<xsl:if test="normalize-space($class)"> <xsl:attribute name="class"> <xsl:value-of select="normalize-space($class)" /> </xsl:attribute> </xsl:if>
Поэтому я использую один и тот же шаблон, делающий эту проверку и позволяющий выводить любые атрибуты:
<xsl:template name="attributes"> <xsl:param name="set" /> <xsl:for-each select="exsl:node-set($set)/*"> <xsl:if test="normalize-space(.)"> <xsl:attribute name="{name(.)}"> <xsl:value-of select="normalize-space(.)" /> </xsl:attribute> </xsl:if> </xsl:for-each> </xsl:template> <xsl:call-template name="attributes"> <xsl:with-param name="set"> <id>NBC</id> <class> warner brothers </class> <galleryimg>no</galleryimg> </xsl:with-param> </xsl:call-template>
Этот же шаблон позволяет не думать о лишних пробелах в CSS-классах, которые в XSL часто собираются из кусочков, ибо эту задачу трима строки выполняет normalize-space().
Таким образом, у нас есть способ передавать в шаблон любой сколь угодно сложный параметр, а не городить кучу параметров примитивного типа. Это похоже на рефакторинг Introduce Parameter Object. Длинный список параметров — один из признаков кода с душком, от которого лучше избавляться, пока не начало вонять. Однако тут главное не переборщить, потому что в объект (временное дерево) нужно объединять логически связанные между собой параметры (в нашем случае это были HTML-атрибуты), а не все подряд, лишь бы сократить количество параметров.
Еще одной демонстрацией сложных параметров может служить задача оборачивания произвольного контента в какую-либо обертку. Проблема в том, что этот контент может быть, а может и отсутствовать, и тогда обертку, логично, выводить тоже не надо. Такие ситуации возникают регулярно. Вот, например, блок дополнительных услуг к товару при оформлении заказа:
В HTML это может выглядеть так:
<div class="services"> <h2>Включить в счет</h2> <div class="service"> <!-- Разметка конкретной услуги --> </div> <div class="service"> <!-- Разметка конкретной услуги --> </div> ... </div>
Понятно, что если нагрузочных услуг у товара нет (ну куклу Барби я решил купить, нельзя, что ли?), то и заголовок <h2>, и обертку <div class="services"> выводить не надо. Конечно, мы можем написать условие, проверяющее, есть ли у товара хотя бы одна дополнительная услуга, однако на деле это может стать непростой задачей, потому что эти дополнительные услуги может строить вообще другой шаблон, и он же будет принимать решение, есть услуга или нет. В данном случае услуги логически разные, и их строит не один, а несколько разных шаблонов.
Самое универсальное решение этой проблемы — предварительно сохранить работу отдельных шаблонов в переменную, а затем проверить, не пустая ли она:
<xsl:variable name="services"> <xsl:call-template name="warranty" /> <xsl:call-template name="insurance" /> <xsl:call-template name="installation" /> </xsl:variable> <xsl:if test="normalize-space($services)"> <div class="services"> <h2>Включить в счет</h2> <xsl:copy-of select="$services" /> </div> </xsl:if>
Это уже похоже на решение задачи в общем виде. Мне не нравится только одно: подобные проверки нужно делать постоянно, то тут, то там. Однажды мне это тоже надоело, и я решил сделать общий шаблон. Ему нужно передавать контент, который может отсутствовать (заранее мы этого не знаем, потому что его построение происходит далеко), и саму обертку. Вот только как ему сказать, где внутри этой обертки располагается неизвестный контент? Элементарно — временные деревья спешат на помощь. Просто в нужное место необходимо положить элемент с заранее определенным именем, например <put_content_here>. Это и будет сложный параметр — обертка с отмеченным внутри нее местом под контент.
Вызов этого шаблона в нашем случае будет выглядеть так:
<xsl:call-template name="wrap"> <xsl:with-param name="content"> <xsl:call-template name="warranty" /> <xsl:call-template name="insurance" /> <xsl:call-template name="installation" /> </xsl:with-param> <xsl:with-param name="wrap"> <div class="services"> <h2>Включить в счет</h2> <put_content_here /> <!-- Здесь вывести контент, если он есть --> </div> </xsl:with-param> </xsl:call-template>
А определение шаблона — так:
<xsl:template name="wrap"> <xsl:param name="content" /> <xsl:param name="wrap" /> <!-- Что-то выводим, только если контент не пуст --> <xsl:if test="normalize-space($content)"> <xsl:apply-templates select="exsl:node-set($wrap)/node()" mode="wrap_copy"> <xsl:with-param name="content" select="$content" /> </xsl:apply-templates> </xsl:if> </xsl:template> <!-- Полностью копируем разметку обертки --> <xsl:template match="*" mode="wrap_copy"> <xsl:param name="content" /> <xsl:element name="{name(.)}"> <xsl:copy-of select="@*" /> <xsl:apply-templates mode="wrap_copy"> <xsl:with-param name="content" select="$content" /> </xsl:apply-templates> </xsl:element> </xsl:template> <!-- Но элемент <put_content_here /> подменяем на контент из параметра $content --> <xsl:template match="put_content_here" mode="wrap_copy"> <xsl:param name="content" /> <xsl:copy-of select="$content" /> </xsl:template>
Теперь эти три шаблона можно положить
Следующая часть — про шаблонизацию строк.
© 19952024 Студия Артемия Лебедева
|