• Техногрет
  • Применение временных деревьев № 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