![]() |
Александр Самиляк
20 июля 2011 |
|
![]() |
Задача. | Разобрать по косточкам теорию и практику использования временных деревьев в XSLT. |
![]() |
![]() |
![]() |
В 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-выражениях, можно и для них объявить неймспейс по умолчанию. Делается это с помощью атрибута
<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>
Есть, правда, одна маленькая неприятность — этот атрибут
Короче, все это аналогично отправляется к мазохистам, но к другим — дополнительно оснащенным трансформатором с поддержкой XSLT 2.0.
Следующий номер нашей программы: решение третье — указать
<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> мы создали сами статическим кодом, поэтому смогли без проблем задать ему
И наконец, решение четвертое — победитель сегодняшнего конкурса решений. Мы можем просто взять и к чертям удалить это объявление неймспейса по умолчанию (тем более что при выводе 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-файлах объявлять неймспейс по умолчанию, либо во всех не объявлять. Временные деревья — главная причина, по которой я не объявляю.
Продолжая мусолить наш пример с файлом для скачивания, вспомним тот исходный шаблон, который выводит 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, начинающийся с корня и желающий получать данные именно из входящего
Последняя добавка для поклонников сущностей. Определив сущность, начинающуюся с корня дерева:
<!ENTITY ext "/extensions">
и использовав ее в нашем
$input_root/&ext;/*[name() = current()/@ext]
мы рискуем после разворачивания сущности получить неожиданный результат:
$input_root//extensions/*[name() = current()/@ext]
// — нехороший оператор, который, строго говоря, делает не то, что нам нужно, поэтому уберем один лишний слеш:
$input_root&ext;/*[name() = current()/@ext]
А вот это уже то, что нам нужно, и это вполне корректная конструкция.
Можно добавить логические скобочки по вкусу:
($input_root)&ext;/*[name() = current()/@ext]
© 19952023 Студия Артемия Лебедева
|