понедельник, 8 марта 2010 г.

XSLT/XPATH Tips & Tricks

Это пост-памятка про различные нюансы XSLT/XPATH. Ничего секретного, просто компиляция из разных источников. Примеры использовал в основном чужие.

  1. Поиск предка лучше реализовывать не через ../../../ а через ancestor::foo[1] — такой подход будет работать и в случае, если искомый элемент изменил своё положение.
  2. По возможности, не следует пользоваться конструкцией // так как её использование ведет к потере производительности. Процессор вынужден будет просмотреть всё дерево документа целиком.
  3. По возможности, не следует пользоваться конструкцией .// так она часто употребляется неверно. Пример – требуется найти первого потомка с именем foo в текущем элементе. Ответ .//foo[1] неверен. Запись .//foo является короткой формой self::node()/descendant-or-self::node()/child::foo соответственно .//foo[1] означает “все потомки с именем foo, каждый из которых первый у своего родителя”. Т.е. возможен возврат более одного элемента. Правильный ответ — descendant::foo[1]. В большинстве случаев именно это и требуется. Другое возможное решение проблемы получения лишних узлов — всегда использовать скобки если в xpath-выражении есть хоть один слеш и оно применяется вне конструкции <xsl:value-of select="" />. Т.е. второй правильный ответ — (.//foo)[1]. Конструкция <xsl:value-of select="" /> вызывает функцию string, которая возвращает текстовое представление только первого узла из отобранных.
  4. Существует красивый способ применения оси self. Например following-sibling::*[1][name() = 'foo'] можно заменить на following-sibling::*[1]/self::foo. Т.е. убираем предикат.
  5. Достаточно распространена ошибка с непониманием работы оператора !=. Особенно при попытке эмулировать ветку else для xsl:if. Это не отрицание, это условие отбора всех узлов для которых не выполняется некоторое условие. А узлов ведь может и не быть. Отрицание корректно задается функций not().
  6. Также многие не понимают оператор =. Это не только проверка на равенство, но и проверка на наличие в списке. Хороший пример: person[@name = $vip/person/@name] позволяет найти людей, у которых имя встречается в списке $vip (а не равно всему списку сразу, как кажется).
  7. Как проверить, что два узла есть один и тот же узел. Первый вариант — count($foo | $bar) = 1. Второй вариант — generate-id($foo) = generate-id($bar).
  8. Как элегантно писать шаблоны с внутренними условиями без громоздкого xsl:choose. Говорящий пример:
    <xsl:template match="Brands" />

    <xsl:template match="Brands[Brand]">
      <h2>Производители</h2>
      <ul>
        <xsl:apply-templates select="Brand" />
      </ul>
    </xsl:template>

    <xsl:template match="Brands[count(Brand) > 10]">
      <h2>Много производителей</h2>
      <!-- Выводим бренды в три колонки -->
    </xsl:template>
  9. Как посчитать среднее. <xsl:variable name="avg" select="sum(/list/item) div count(/list/item)"/>
  10. Как найти минимум/максимум — отсортировать по возрастанию/убыванию и взять первую.
    <xsl:variable name="min">
       <xsl:for-each select="/list/item">
          <xsl:sort data-type="number" order="ascending"/>
          <xsl:if test="position() = 1"><xsl:value-of select="." /></xsl:if>
       </xsl:for-each>
    </xsl:variable>
  11. current() в предикате позволяет вернуться к контексту текущего узла. То есть вместо того, чтобы создавать переменную:
    <xsl:template match="navigation">
       <xsl:variable name="status" select="@status" />
       <xsl:value-of select="/captions/caption[@status = $status]" />
    </xsl:template>
    гораздо удобнее писать
    <xsl:template match="navigation">
       <xsl:value-of select="/captions/caption[@status = current()/@status]" />
    </xsl:template>
  12. Вместо того чтобы писать несколько xsl:value-of подряд, можно использовать concat(). Например <xsl:value-of select="concat(@Name, ' ', @Surname)" />
  13. Нужно помнить, что всегда выполняется следующий порядок:
    preceding-sibling[last()]
    ...
    preceding-sibling[2]
    preceding-sibling[1]
    current item
    following-sibling[1]
    following-sibling[2]
    ...
    following-sibling[last()]
    Но! position() после <xsl:sort> возвращает позицию в отсортированном дереве.
  14. Также нужно помнить что call-template, в отличие от apply-templates, не меняет контекстный (текущий) узел.
  15. При написании тестов узлов важно не забывать о богатстве возможностей предикатов. Вот хороший пример. <xsl:template match="news[region/person[starts-with(., 'A')]]" />. Или вот — <xsl:apply-template select="comment[@status = 'real'][position() &lt;= 3]" />

Prathit Bondre написал интересный документ XSLT – Efficient Programming Techniques. Есть перевод. В этом документе рассмотрены весьма полезные приемы:

  1. Подключение внешних файлов с данными. Через DOCTYPE ENTITY, через функцию document(). Я сам применяю первый способ для подключения мелких файлов, нужно только помнить, что на эти файлы распространяется ряд неприятных ограничений. Например, при подключении js-скрипта в файле не должно быть символа &.
  2. Пересечение, разность и дизъюнкция множеств.
  3. Нерекурсивные циклы.
  4. Условная выборка в xpath.
  5. Классическая группировка по Мюнху.
  6. Библиотека готовых функций http://xsltsl.sourceforge.net/. Это не EXSLT.

Подключение ENTITY также интересно рассмотрено у Сергея Чикуенка. Он применяет их для эмуляции переменных в тестах узлов шаблонов.

Ну и на закуску. Как получить кросс-процессорную функцию node-set. Использую этот метод давно, отлично работает.