Что такое временное дерево

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

21 июня 2011


Задача.

Разобрать по косточкам теорию и практику использования временных деревьев в XSLT.

XSL-трансформация имеет XML-дерево на входе (входящее дерево) и производит дерево на выходе. Однако в коде шаблона могут создаваться свои деревья, не являющиеся частью входящего и не подлежащие выводу. Это и есть временные деревья.

Вспомним синтаксис элементов <xsl:variable>, <xsl:param> и <xsl:with-param>. У них можно указать атрибут select, при этом тело элемента должно быть пустым. Тогда тип переменной будет строкой (select="'Chicago, Illinois'"), числом (select="331"), булевым типом (select="true()") или набором узлов node-set (select="//episode").

Однако можно не указывать атрибут select, а присвоить значение в теле элемента. Вот тогда в переменной (или параметре) будет временное дерево. Его можно создать статическим кодом:

 <xsl:variable name="ER_characters">
  <item>Mark Greene</item>
  <item sexy="true">Doug Ross</item>
  <item>Susan Lewis</item>
  <item>John Carter</item>
</xsl:variable>

А можно и вызовом другого шаблона, который возвращает какой-либо заранее неизвестный XML:

 <xsl:variable name="ER_characters">
  <xsl:call-template name="get_ER_characters" />
</xsl:variable>

Называть такое хозяйство временным деревом первым стал Майкл Кэй (небритый мужик на обложке красной книжки XSLT). Всем понравилось, и термин temporary tree решили даже включить в спецификацию XSLT 2.0. Да-да, вы правильно начали волноваться. В XSLT 1.0 то, что находится в теле элемента <xsl:variable>, временным деревом на тот момент не признали и обозвали странными словами Result Tree Fragment (RTF).

С этим фрагментом результирующего дерева нельзя сделать ничего, кроме примитивных строковых операций. То есть можно сделать, например, copy-of, что уже радует. Это позволяет писать общие шаблоны, занимающиеся оборачиванием какого-либо XML в заранее определенные обертки. Пример — вывод произвольного HTML-контента, снабженного скругленными уголками:

 <xsl:template name="rounded_corners">
  <xsl:param name="content" />
  
  <xsl:if test="string($content)">
    <div class="rounded">
      <b class="c lt" /><b class="c rt" />
      <xsl:copy-of select="$content" />
      <b class="c rb" /><b class="c lb" />
    </div>
  </xsl:if>
</xsl:template>
  
  
<xsl:call-template name="rounded_corners">
  <xsl:with-param name="content">
    <!-- Это временное дерево -->
    <p>
      <b>ER</b> is an American medical drama television series aired on NBC.
    </p>
  </xsl:with-param>
</xsl:call-template>

Однако на этом радость кончается, ведь в XSLT 1.0 мы не можем обойти временное дерево (тьфу, то есть RTF) XPath-ом и получить какую-либо его внутреннюю часть. Поэтому написать так:

 <xsl:variable name="ER_cast">
  <item>Anthony Edwards</item>
  <item sexy="false">George Clooney</item>
  <item>Sherry Stringfield</item>
  <item>Noah Wyle</item>
</xsl:variable>
  
<xsl:value-of select="$ER_cast/item[3]" />

не получится. Путь к светлому будущему нам преградит ошибка (на примере трансформатора libxslt):

Invalid type runtime error: file ... line ...
element value-of XPath evaluation returned no result

А все потому, что в спецификации 1.0 черненькими буковками по беленькому фончику написано: «Операции / или [ ] недопустимы для типа данных RTF». Ну производители XSL-трансформаторов в это дело невинно поверили и так всё и запрограммировали. Хочешь попасть внутрь временного дерева? На тебе эксэпшен.


Можно было уже начинать паковать чемоданы, как выяснилось, что не все готовы мириться с таким произволом властей. Некоторые ребята из XSL-сообщества собрались и подумали: «А почему бы нам не сделать такую функцию, которая будет преобразовывать тип RTF в тип node-set?» (Тот самый тип, который возвращается элементам <xsl:variable>, <xsl:param> и <xsl:with-param> с атрибутом select.)

Это ли не идея судьбы? Ведь данные типа node-set позволяют обойти себя любым XPath-ом и динамически вычленить нужные элементы. Сказано — сделано. Так на свете появилась функция node-set из проекта расширений EXSLT. Я подозреваю, что в первую очередь эта функция node-set сделала весь проект таким известным. Настолько известным, что производители трансформаторов все как один кинулись реализовывать разные модули EXSLT в своих детищах.

Весь проект EXSLT разделен на модули, которые содержат наборы функций определенного предназначения — common, dates and times, math... Функция node-set находится в модуле common.


Итак, что нужно делать, чтобы праздник пришел в каждый XSL-шаблон:

 <xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
>
  
  <xsl:variable name="ER_cast">
    <item>Anthony Edwards</item>
    <item sexy="false">George Clooney</item>
    <item>Sherry Stringfield</item>
    <item>Noah Wyle</item>
  </xsl:variable>
  
  <xsl:template match="/">
    <xsl:value-of select="exsl:node-set($ER_cast)/item[3]" />
  </xsl:template>
  
</xsl:stylesheet>

Хозяйке на заметку

Этот и все последующие примеры предполагают, что в XSL-файле не объявлен неймспейс по умолчанию (например, xmlns="http://www.w3.org/1999/xhtml").

Об этой и еще одной тонкости работы с временными деревьями можно почитать в отдельной части.

Мы объявили неймспейс с префиксом exsl и URI, равным http://exslt.org/common. Этот URI зашит в трансформатор, и, видя такой URI, он сразу понимает, что надо подключить модуль common библиотеки расширений EXSLT. Префикс может быть любым, это внутреннее имя шаблона, но мы используем exsl.

Надо сказать, что это стандартный способ подключения к XSL-шаблону сторонних модулей — уникальный URI этого стороннего модуля присваивается выбранному префиксу, а дальше в нужном месте вызывается функция этого модуля конструкцией вида:

префикс : имя функции (аргументы)

Назад к примеру. Мы создаем временное дерево и сохраняем его в переменную $ER_cast, которая получает тип RTF. Затем преобразуем эту переменную к типу node-set через вызов exsl:node-set($ER_cast) и хотим получить третий элемент <item> этого только что созданного node-set-а.

И вот это, товарищи, уже работает. На выходе получаем заветную строчку "Sherry Stringfield".


Внимательный читатель в этом примере заметит важное: на первом уровне временного дерева может находиться более одного элемента (у нас 4 элемента <item>), тогда как на первом уровне дерева входящего всегда должен быть один и только один корневой элемент, чего требует XML-спецификация. На первом уровне временного дерева даже может находиться текстовый узел, но тогда во всех XPath-выражениях переменная, содержащая такое временное дерево, будет конвертироваться в обычную строку.

Также замечу, что упомянутое выражение exsl:node-set($ER_cast) возвращает именно корень временного дерева, а не node-set из 4 элементов <item>, как можно подумать.


В следующей части мы займемся применением временных деревьев.

1
2
3
4
5
6
7
8
9