![]() |
Александр Самиляк
30 июня 2011 |
|
![]() |
Задача. | Разобрать по косточкам теорию и практику использования временных деревьев в XSLT. |
![]() |
![]() |
![]() |
Представим, что нам надо сделать вот такую галерею:
Обращаем внимание на ссылки вида «еще x фотографий и y видео» и думаем, как бы нам их собрать. Будем считать, что данные о количестве фотографий и видео у нас есть в XML:
<galleries> <gallery> <name>Интерьеры</name> <preview>...</preview> </gallery> <gallery> <name>Экстерьеры</name> <preview>...</preview> <more photos="4" videos="2" /> </gallery> <gallery> <name>Окрестности</name> <preview>...</preview> <more photos="34" videos="8" /> </gallery> </galleries>
Способ в лоб — делаем шаблон с двумя параметрами photos, videos и старательно выводим в нем каждое слово, одно за другим. Не забываем о том, что сайт может быть многоязычным, поэтому данные, зависящие от языка, лучше положить во входящий XML (пусть это будут элементы <label>) через админку. Не будем волноваться и решим, что в собираемой нами строке всегда будут и фото, и видео. Вот что должно получиться:
<xsl:variable name="labels" select="//label" /> <xsl:template name="make_more_text"> <xsl:param name="photos" /> <xsl:param name="videos" /> <!-- еще --> <xsl:value-of select="$labels[@for = 'more']" /><xsl:text> </xsl:text> <!-- 34 --> <xsl:value-of select="$photos" /><xsl:text> </xsl:text> <!-- фотографии --> <xsl:value-of select="$labels[@for = 'photos']" /><xsl:text> </xsl:text> <!-- и --> <xsl:value-of select="$labels[@for = 'and']" /><xsl:text> </xsl:text> <!-- 8 --> <xsl:value-of select="$videos" /><xsl:text> </xsl:text> <!-- видео --> <xsl:value-of select="$labels[@for = 'video']" /> </xsl:template> <xsl:template match="gallery"> <h2> <xsl:value-of select="name" /> </h2> ... <span class="pseudo more"> <xsl:call-template name="make_more_text"> <xsl:with-param name="photos" select="more/@photos" /> <xsl:with-param name="videos" select="more/@videos" /> </xsl:call-template> </span> </xsl:template>
Написав это, понимаем, что оно никуда не годится.
Поэтому, отправив написанное в топку, чешем репу дальше. Чешем и вспоминаем, что XSL — это же шаблонизатор, черт возьми. В результате приходим к способу второму, не в лоб (который хоть и не очевиден, но известен давно). Вместо десятка отдельных лейблов нам нужно собрать один, пометив в нем тегами те места, в которые нужно вставить числа:
<label for="more_template">еще <photos /> фотографии и <videos /> видео</label>
Язык наш велик и могуч, поэтому окончания слов после числительных меняются в зависимости от числа, притом черт знает как. Эх, встретить бы того паразита, который это придумал... Короче, дорабатываем наш лейбл, добавляя еще парочку и не забывая про типографику:
<!-- Этот XML будет меняться в админке для каждого языка на сайте --> <label for="more_template"> еще <nobr><photos /> <photo_word /></nobr> <nobr>и <videos /> <video_word /></nobr> </label> <label for="photo_word_forms"> <form>фотография</form> <form>фотографии</form> <form>фотографий</form> </label> <label for="video_word_forms"> <form>видео</form> <form>видео</form> <form>видео</form> </label>
Вот теперь мы защищены от любых невзгод. При такой схеме мы можем в разных языковых версиях сайта без труда менять порядок слов (скажем, написать 4 další fotky a 2 videa
Покончив с данными, перейдем к коду. Нам нужно отправить на рекурсивную обработку содержимое лейбла <label for="more_template">. При этом особые элементы (<photos>, <photo_word>, <videos>, <video_word>) нужно заменить на числа со словами в соответствующих падежах, а остальные элементы (<nobr>) и текстовые узлы — скопировать без изменений.
Начнем с простого рекурсивного обхода. В первом приближении он выглядит так:
<!-- Заведем специальный mode="html", который выводит конечную разметку --> <xsl:template match="*" mode="html"> <xsl:element name="{name()}"> <xsl:copy-of select="@*" /> <xsl:apply-templates mode="html" /> </xsl:element> </xsl:template>
Осталось сделать сущие пустяки — отправить наш <label for="more_template"> на обработку с mode="html" и перехватить интересующие нас элементы, чтобы заменить их реальными данными. Попробуем:
<xsl:template match="gallery"> <h2> <xsl:value-of select="name" /> </h2> ... <span class="pseudo more"> <xsl:apply-templates select="$labels[@for = 'more_template']/node()" mode="html" /> </span> </xsl:template> <xsl:template match="photos[ancestor::label[@for = 'more_template']]" mode="html"> <!-- WTF? И откуда нам тут взять число фотографий? --> </xsl:template>
Черт, очередная засада. Мы уперлись в то, что в теле последнего шаблона mode="html" мы не знаем, о какой галерее выводим сейчас информацию. То есть мы просто заматчили элемент <photos>, находящийся в лейбле, но в контексте какой галереи мы находимся, неизвестно. Как бы нам сообщить в этот шаблон, какую галерею мы сейчас обрабатываем? Первая мысль — через параметр:
<xsl:template match="gallery"> ... <xsl:apply-templates select="$labels[@for = 'more_template']/node()" mode="html"> <xsl:with-param name="gallery" select="." /> </xsl:apply-templates> </xsl:template> <xsl:template match="photos[ancestor::label[@for = 'more_template']]" mode="html"> <xsl:param name="gallery" /> <xsl:value-of select="$gallery/more/@photos" /> </xsl:template>
Но тут тоже есть подводный камень. Шаблон mode="html" рекурсивно обходит отправленный ему узел, поэтому наш параметр дойдет до адресата (match="photos") только в том случае, если элемент <photos> находится на первом уровне нашего лейбла и не обернут в другие элементы. А если обернут, как в нашем случае в <nobr>, то сначала сработает шаблон:
<!-- Здесь под звездочкой в match="*" подразумеваем nobr --> <xsl:template match="*" mode="html"> <xsl:element name="{name()}"> <xsl:copy-of select="@*" /> <xsl:apply-templates mode="html" /> </xsl:element> </xsl:template>
который про параметр gallery ничего не знает, поэтому детей элемента <nobr> (в том числе и наш элемент <photos>) он отправит на обработку без этого параметра.
Но мы ведь можем усовершенствовать наш шаблон mode="html", чтобы он умел принимать какой-то абстрактный параметр и передавать его глубже:
<xsl:template match="*" mode="html"> <xsl:param name="params" /> <xsl:element name="{name()}"> <xsl:copy-of select="@*" /> <xsl:apply-templates mode="html"> <xsl:with-param name="params" select="$params" /> </xsl:apply-templates> </xsl:element> </xsl:template>
Теперь воспользуемся этим параметром params. Надо только подумать, чтó в этот params отдать, потому что вывести нам надо две вещи (количество фотографий и количество видео), а параметр только один (мы же сделали шаблон общего значения, который не может думать о том, сколько понадобится параметров в каждом частном случае — всегда найдется ситуация, при которой объявленного количества параметров будет мало).
Пожалуй,
<xsl:template match="gallery"> ... <xsl:apply-templates select="$labels[@for = 'more_template']/node()" mode="html"> <xsl:with-param name="params" select="more" /> </xsl:apply-templates> </xsl:template> <xsl:template match="photos[ancestor::label[@for = 'more_template']]" mode="html"> <xsl:param name="params" /> <xsl:value-of select="$params/@photos" /> </xsl:template> <xsl:template match="photo_word[ancestor::label[@for = 'more_template']]" mode="html"> <xsl:param name="params" /> <xsl:call-template name="word_forms"> <xsl:with-param name="number" select="$params/@photos" /> <xsl:with-param name="forms" select="$labels[@for = 'photo_word_forms']/form" /> </xsl:call-template> </xsl:template> <!-- И то же самое для видео --> ...
Таким образом, этот params передается каждому узлу, обрабатываемому с mode="html" и доходит до наших элементов-адресатов, как бы глубоко они ни лежали. Доставка «ФедЭкс» просто.
А при чем тут временные деревья? А при том, что у описанного метода есть маленький недостаток — необходимость принимать параметр params и передавать его глубже. Дело в следующем. Сейчас у нас только один шаблон mode="html", который матчит любые элементы (match="*") и просто отправляет их на вывод без изменений. Но завтра мы можем захотеть определенные HTML-элементы обрабатывать особым образом. Скажем, выводить ссылку <a>, только если она не ведет на текущую страницу (сама на себя), в противном случае показывать лишь ее текст без ссылки. Во всех таких случаях мы должны помнить, что надо принять и передать глубже параметр params:
<xsl:template match="a" mode="html"> <xsl:param name="params" /> <xsl:choose> <xsl:when test="@href = $current_page_href"> <xsl:apply-templates mode="html"> <xsl:with-param name="params" select="$params" /> </xsl:apply-templates> </xsl:when> <xsl:otherwise> <a> <xsl:copy-of select="@*" /> <xsl:apply-templates mode="html"> <xsl:with-param name="params" select="$params" /> </xsl:apply-templates> </a> </xsl:otherwise> </xsl:choose> </xsl:template>
В реальном проекте таких шаблонов обычно разводится пруд пруди. Тут надо поставить особый класс у абзаца <p>, идущего перед списком <ul> (потому что IE6, глупец, не понимает
Итак, если мы передали параметр params, но он не пришел в конечный шаблон и мы не можем найти, в каком из тридцати шаблонов mode="html" прервалась цепочка, есть обходной путь. Мы можем собрать временное дерево, содержащее шаблонизируемую строку (<label for="more_template">) и данные (<more photos="…" videos="…">), которые необходимы для расчета чисел, вставляемых в шаблон:
<xsl:template match="gallery"> ... <xsl:variable name="template_with_data"> <xsl:copy-of select="$labels[@for = 'more_template']" /> <xsl:copy-of select="more" /> <!-- Здесь можно не просто скопировать фрагмент входящего XML, а провести любой расчет --> </xsl:variable> <xsl:apply-templates select="exsl:node-set($template_with_data)/label/node()" mode="html" /> </xsl:template> <xsl:template match="photos[ancestor::label[@for = 'more_template']]" mode="html"> <xsl:value-of select="/more/@photos" /> </xsl:template>
Мы собрали вместе лейбл с шаблонной строкой и данные, которые мы вставляем в эту шаблонную строку. Теперь они находятся в одном временном дереве, и кроме них в этом дереве больше ничего нет. Выглядит оно примерно так:
Дальше — самое интересное. Когда мы матчим элемент <photos> в глубине этого временного дерева (обведен красным), мы с помощью XPath "/more/@photos" поднимаемся вверх до корня дерева, заходим в элемент <more> первого уровня и забираем из него нужные нам данные.
Таким образом, мы передали контекст исполнения в шаблон <xsl:template match="photos[…]" mode="html"> не параметром params, а тем деревом, в котором сейчас находится элемент <photos>. Для каждой галереи это дерево будет своим.
При таком подходе params мы не используем, и нам не страшно, что он где-то может потеряться.
В этом примере хочу обратить внимание читателя на важную вещь. Оператор / в начале
Если мы, находясь в контексте временного дерева, хотим получить данные из входящего, то нужно за пределами временного дерева (лучше всего где-нибудь на первом уровне XSL-шаблона) предварительно создать переменную, указывающую на корень входящего дерева:
<xsl:variable name="input_root" select="/" />
Далее из контекста временного дерева обращаться к входящему можно по $input_root.
И последнее. Если мы отправляем на обработку все временное дерево целиком:
<xsl:apply-templates select="exsl:node-set($var)" mode="process" />
то матчить надо именно корень:
<xsl:template match="/" mode="process"> ... </xsl:template>
В очередной части я расскажу про следующее применение временных деревьев — подстройку под готовый шаблон.
© 19952025 Студия Артемия Лебедева
|