Тонкости использования временных деревьев

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

20 июля 2011


Задача.

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

Тонкость № 1 — неймспейс по умолчанию

В XSL-шаблоне может быть объявлен неймспейс по умолчанию. Например, такой:

xmlns="http://www.w3.org/1999/xhtml"

Подробно о неймспейсах настоятельно рекомендую почитать отдельную статью, а здесь напомню, что неймспейс по умолчанию получают все элементы, которые созданы в коде шаблона и не имеют своего явного неймспейса. А временные деревья как раз и создаются в коде шаблона и обычно не имеют своего неймспейса. Здесь могут быть проблемы. Поясню на примере.

Вспомним, что мы делали в применении № 4. Мы там выводили файл для скачивания:

 <File
  fullname="/r/report.doc"
  name="report.doc"
  extension="DOC"
  pretty_size="43 KБ"
>
  <label>Квартальный отчет</label>
</File>

подстроившись с помощью временного дерева под готовый шаблон:

 <xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
>
  
<xsl:template match="File[@fullname and (label or @name)]" mode="html">
  <xsl:variable name="file">
    <file
      src="{@fullname}"
      name="{@name}"
      ext="{@extension}"
      size="{@pretty_size}"
    >
      <xsl:copy-of select="label/node()" />
    </file>
  </xsl:variable>
  
  <xsl:apply-templates select="exsl:node-set($file)/file" mode="html" />
</xsl:template>
  
</xsl:stylesheet>

А теперь сюрприз: этот XSL ничего не выведет. И вот почему.

В теле переменной $file мы создали временное дерево, состоящее из одного элемента <file>. У этого элемента нет явного неймспейса, поэтому он получил неймспейс по умолчанию, коим в начале шаблона объявлен XHTML-неймспейс. Далее с помощью exsl:node-set($file) мы превращаем временное дерево в полноправный набор узлов и хотим выбрать в нем только что созданный элемент <file>. И вот в этом месте и происходит осечка. Дело в том, что XPath select="file" выбирает все элементы, у которых имя равно file и неймспейс равен null (и это не зависит от того, какой объявлен неймспейс по умолчанию). А неймспейс нашего элемента <file> не равен null, потому-то ничего и не работает.

Проблема ясна, теперь разберемся, как ее решать. А решений есть на удивление много.

Решение первое — указать в XPath, что нас интересует конкретный XHTML-неймспейс, а не null. Делается это так:

 <xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xhtml="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
>
  
  <xsl:template match="/">
    ...
    <xsl:apply-templates
      select="exsl:node-set($file)/xhtml:file"
      mode="html"
    />
  </xsl:template>
  
</xsl:stylesheet>

Вот теперь элемент <file> успешно выберется. Но этим, правда, дело не кончится. Ведь, будучи выбранным, элемент <file> должен отправиться на обработку в шаблон-матч, в котором написано что-то вроде этого:

 <xsl:template match="file" mode="html">
  ...
</xsl:template>

Для XPath внутри match действуют те же правила, что и внутри select, поэтому match="file" захватит только элемент <file>, имеющий неймспейс null. А у нас не null. Чтобы это заработало, шаблон-матч надо тоже править:

 <xsl:template match="file | xhtml:file" mode="html">
  ...
</xsl:template>

Сказать, что все это страшно неудобно — ничего не сказать, поэтому оставляем это решение всяким мазохистам.


Решение второе — чтобы явно не указывать неймспейс в XPath-выражениях, можно и для них объявить неймспейс по умолчанию. Делается это с помощью атрибута xpath-default-namespace:

 <xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
  xpath-default-namespace="http://www.w3.org/1999/xhtml"
>
  
  <xsl:template match="/">
    ...
    <xsl:apply-templates select="exsl:node-set($file)/file" mode="html" />
  </xsl:template>
  
</xsl:stylesheet>

Есть, правда, одна маленькая неприятность — этот атрибут xpath-default-namespace появился только в XSLT 2.0, так что скорее всего трансформатор будет не в восторге. Кроме того, желательно, чтобы входящий XML также находился в XHTML-неймспейсе, иначе во всех XPath-выражениях, касающихся входящего XML, придется писать явный неймспейс.

Короче, все это аналогично отправляется к мазохистам, но к другим — дополнительно оснащенным трансформатором с поддержкой XSLT 2.0.


Следующий номер нашей программы: решение третье — указать null-неймспейс при создании временного дерева.

 <xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
>
  
  <xsl:template match="File[@fullname and (label or @name)]" mode="html">
    <xsl:variable name="file">
      <file
        xmlns=""
        src="{@fullname}"
        name="{@name}"
        ext="{@extension}"
        size="{@pretty_size}"
      >
        <xsl:copy-of select="label/node()" />
      </file>
    </xsl:variable>
  
    <xsl:apply-templates select="exsl:node-set($file)/file" mode="html" />
  </xsl:template>
  
</xsl:stylesheet>

Это решение, на мой взгляд, сильно лучше, чем те, что были до этого — не нужно разводить никакой грязи в XPath-выражениях. Правда, стоит заметить, что элемент <file> мы создали сами статическим кодом, поэтому смогли без проблем задать ему null-неймспейс. Если же временное дерево генерировалось бы в другом месте, а к нам попадало, скажем, через вызов промежуточного шаблона — нам пришлось бы править то другое место, что не очень хорошо.


И наконец, решение четвертое — победитель сегодняшнего конкурса решений. Мы можем просто взять и к чертям удалить это объявление неймспейса по умолчанию (тем более что при выводе HTML его необходимость весьма сомнительна):

 <xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
>
  
  <xsl:template match="File[@fullname and (label or @name)]" mode="html">
    <xsl:variable name="file">
      <file
        src="{@fullname}"
        name="{@name}"
        ext="{@extension}"
        size="{@pretty_size}"
      >
        <xsl:copy-of select="label/node()" />
      </file>
    </xsl:variable>
  
    <xsl:apply-templates select="exsl:node-set($file)/file" mode="html" />
  </xsl:template>
  
</xsl:stylesheet>

В этом случае все элементы временного дерева изначально имеют неймспейс null и отлично выбираются, а затем матчатся в XPath-выражениях.

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


Тонкость № 2 — доступ во входящее дерево из временного

Продолжая мусолить наш пример с файлом для скачивания, вспомним тот исходный шаблон, который выводит HTML-разметку для файла:

 <xsl:template match="file[@src and (string(.) or @name)]" mode="html">
  <span class="file {@ext}">
    ...
    <nobr>
      <xsl:value-of select="concat(@ext, ', ', @size)"/>
    </nobr>
  </span>
</xsl:template>

Не вдаваясь во второстепенные подробности, обращаем внимание на последнюю часть шаблона, которая выводит расширение файла и его объем. Получается HTML вида:

 <nobr>PDF, 43 КБ</nobr>

Расширение файла берется из элемента, пришедшего на обработку, точнее, из его атрибута ext. А теперь представим, что возникла задача писать не PDF, а ПДФ, как это делается у нас на design.ru. Не вопрос — делаем во входящем XML словарик соответствия популярных расширений их русским аббревиатурам:

 <extensions>
  ...
  <PDF>ПДФ</PDF>
  <DOC>ДОК</DOC>
  ...
</extensions>

и подменяем английское расширение русским:

 <xsl:template match="file[@src and (string(.) or @name)]" mode="html">
  <span class="file {@ext}">
    ...
    <nobr>
      <xsl:value-of
        select="concat(/extensions/*[name() = current()/@ext], ', ', @size)"
      />
    </nobr>
  </span>
</xsl:template>

Все прекрасно работает, но лишь до тех пор, пока мы не отправим в этот шаблон элемент <file> из временного дерева, которое мы создали, чтобы подстроить новый формат XML под старый шаблон (повторенье — мать ученья, поэтому в десятый раз тот же самый код):

 <File
  fullname="/r/report.doc"
  name="report.doc"
  extension="DOC"
  pretty_size="43 KБ"
>
  <label>Квартальный отчет</label>
</File>
 <xsl:template match="File[@fullname and (label or @name)]" mode="html">
  <xsl:variable name="file">
    <file
      src="{@fullname}"
      name="{@name}"
      ext="{@extension}"
      size="{@pretty_size}"
    >
      <xsl:copy-of select="label/node()" />
    </file>
  </xsl:variable>
  
  <xsl:apply-templates select="exsl:node-set($file)/file" mode="html" />
</xsl:template>

При таком раскладе русское расширение не достанется и мы получим на выходе такой HTML:

 <nobr>, 43 КБ</nobr>

Подумаем, почему так происходит. В шаблоне match="file[…]" mode="html" мы использовали конструкцию:

/extensions/*[name() = current()/@ext]

XPath начинается с оператора /, и вот здесь как раз собака и зарыта. Напомню, что оператор / в начале XPath-выражения возвращает корень того дерева, в котором находится контекстный узел. А контекстным узлом у нас является элемент <file>, находящийся в только что созданном временном дереве, в корне которого нет никаких словариков с расширениями для файлов.

Нам нужно, находясь в контексте временного дерева, вырваться за его пределы и вытащить данные из дерева входящего. Это можно сделать, предварительно сохранив ссылку на корень входящего дерева в переменную:

 <xsl:variable name="input_root" select="/" />
  
<xsl:template match="file[@src and (string(.) or @name)]" mode="html">
  <span class="file {@ext}">
    ...
    <nobr>
      <xsl:value-of
        select="concat($input_root/extensions/*[name() = current()/@ext], ', ', @size)"
      />
    </nobr>
  </span>
</xsl:template>

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

Отсюда правило: XPath, начинающийся с корня и желающий получать данные именно из входящего XML-дерева (а не просто из дерева, содержащего контекстный узел), нужно предварять переменной, которая явно указывает на входящее дерево.


Последняя добавка для поклонников сущностей. Определив сущность, начинающуюся с корня дерева:

<!ENTITY ext "/extensions">

и использовав ее в нашем XPath-е:

$input_root/&ext;/*[name() = current()/@ext]

мы рискуем после разворачивания сущности получить неожиданный результат:

$input_root//extensions/*[name() = current()/@ext]

// — нехороший оператор, который, строго говоря, делает не то, что нам нужно, поэтому уберем один лишний слеш:

$input_root&ext;/*[name() = current()/@ext]

А вот это уже то, что нам нужно, и это вполне корректная конструкция.

Можно добавить логические скобочки по вкусу:

($input_root)&ext;/*[name() = current()/@ext]
1
2
3
4
5
6
7
8
9