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

    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