Применение временных деревьев № 2 — передача сложных параметров

HTML и CSSXSLTJavaScriptИзображенияСофтEtc
Александр Самиляк

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>

Теперь эти три шаблона можно положить в XSL-файл с утилитами и пользоваться везде, где мы заранее не знаем, будет ли контент для показа или не будет.


Следующая часть — про шаблонизацию строк.

1
2
3
4
5
6
7
8
9