Практический XSLT. Использование в качестве шаблонизатора. Часть 2

    В предыдущей статье мы разобрали основные аспекты построения шаблона с помощью XSLT. Однако, для полноценного шаблона нужно не только выводить меню сайта, но также и текстовый материал документа.

    Вывод HTML-контента из XML-документа


    Ранее, мы определили, что все содержание разделов в XML-документе публикуется в блоке . При этом, каждый модуль содержимого помещается в узел .

    Для простоты предположим что пока у нас нет на сайте никакого динамического наполнения: форумов, новостей и д.р. Соответственно, нам на данном этапе достаточно выбрать элементы item у которых указан тип html и раскидать их в шаблоне в соответствии с тем, к какому блоку они относятся (указано в атрибуте container).

    Добавим в шаблон xsl/template.xsl следующий код:

    <xsl:template match="item" mode="html">

    <xsl:value-of select="@title"/>


    <xsl:value-of select="." disable-output-escaping="yes" />
    </xsl:template>


    Мы добавили шаблон для элементов item. В соответствии с ним мы будем для каждого текстового блока выводить его название (атрибут title) и сам текст. Для вывода текста мы воспользуемся инструкцией <xsl:value-of select="." disable-output-escaping="yes" />, в которой мы выбираем текущий элемент (.) и с помощью параметра-атрибута disable-output-escaping мы отключаем "маскирование" входного кода, т.е. текст будет выводиться "как есть". Это нам необходимо для корректного вывода тегов в коде форматирования текста.

    Однако, как правило, наполнение сайта делают люди, которых мало волнует соответствие форматирования текста стандарту XHTML. В случае с использованием XSLT любой незакрытый тэг (тот же <br>) будет вызывать ошибку парсера. Я долгое время пытался придумать разного рода анализаторы кода для чистки вводимого контента (тех-же новостей) на сайт, пока Денис Креминский не подсказал мне одно простое, но весьма эффективное решение: поместить весь опасный код в CDATA.
    Секция CDATA используется для того, чтобы обозначить части документа, которые не должны восприниматься как разметка. Секция CDATA начинается со строки '<![CDATA[' и заканчивается строкой ']]>'. Внутри самой секции не должна присутствовать строка ']]>'.

    Другими словами, все что в CDATA XSL-парсером не анализируется и передается в конечный документ "как есть".

    Теперь, нам нужно указать где нам нужно выводить те или иные модули. Для этого, откроем xsl/my_template/layout.xsl и найдем в где у нас начинается разбивка на блоки.

    Предположим, что у вас в изначальном XHTML-шаблоне была следующая таблица:




    В ней описывается простой двух-колоночный макет. Вырежем данный код (всю таблицу) и поместим ее в файл xsl/template.xsl в новый шаблон:

    <xsl:template match="content">



    </xsl:template>


    Мы создали шаблон для блока content. Соответственно, в основном файле xsl/my_template/layout.xsl нам нужно указать, где он должен вызываться. Для этого вставьте в на место вырезанной таблицы следующий код:

    <xsl:apply-templates select="content"/>


    Осталось только добавить в xsl/template.xsl инструкции для вывода нужных нам блоков:

    <xsl:template match="content">

    <xsl:apply-templates select="item[@container = 1]" mode="html"/>

    <xsl:apply-templates select="item[@container = 2]" mode="html"/>

    </xsl:template>


    В результате, из нашего XML-документа основное наполнение страницы будет аккуратно разложено в соответствующие блоки шаблона.

    Создание под-меню


    Разберемся теперь более подробно со структурой нашего сайта.

    В нашем XML-документе в блоке выводиться все дерево-структура сайта. При этом, для каждого элемента (раздела сайта) у нас предусмотрен атрибут section который задает тип раздела. Однако, наш шаблон для меню навигации пока никак это не учитывает.

    Откроем xsl/navigation.xsl и доработаем немного шаблон <xsl:template match="sections" mode="global_menu">
    :

    <xsl:template match="sections" mode="global_menu">


    </xsl:template>


    Мы указали, что обрабатывать будем только разделы, у которых указан тип section=1.

    Далее, попробуем избавиться от конструкции <xsl:when test="descendant-or-self::*/@id = /node()/@id"> (с помощью нее мы определяли является ли текущим обрабатываемый пункт меню). Ничего плохого в этой конструкции нет, но мы можем ее упростить и сделать более универсально (может кому-то неудобно указывать номер текущего раздела в корневом узле и постоянно его проверять): попробуем передавать id текущего раздела в виде переменной.

    Допустим, блок во входном XML-документе формируется отдельным модулем CMS и этот модуль указывает номер текущего раздела в корневом узле блока : <sections hit_id="2">. Таким образом, в шаблоне для элемента sections перед вызовом обработчиков для item нам нужно им как-то передать номер текущего раздела. Для этого воспользуемся переменными. Переделаем немного строку вызова обработчиков для item:
    <xsl:apply-templates select="item[@section=1]" mode="global_menu">
    <xsl:with-param name="cur"><xsl:value-of select="@hit_id"/></xsl:with-param>
    </xsl:apply-templates>


    Мы поместили внутрь вызова <xsl:apply-templates/> (сделав из него парный элемент) инструкцию <xsl:with-param name="cur">, которая создает параметр-переменную cur в контексте текущего вызова.

    Теперь, упростим немного шаблон <xsl:template match="item" mode="global_menu">:

    <xsl:when test="descendant-or-self::*/@id = $cur">
    <xsl:value-of select="title"/>
    </xsl:when>


    В проверку мы вставили переменную $cur. Некоторые, наверное спросят: а зачем нам descendant-or-self::*/@id, если можно сравнивать просто с @id, т.е. просто идентификатором текущего раздела? Дело в том, что если у раздела есть подразделы и мы попадаем на них, то нам нужно как-то отметить родительский раздел. Конструкция descendant-or-self::*/@id указывает совпадение атрибута id для узла или любого из его потомков.

    Однако, нам нужно предусмотреть два варианта:
    • когда мы находимся в самом разделе - ссылка на раздел не нужна;
    • когда мы находимся в подразделе - ссылку на раздел делаем.

    При этом, в любом случае, нужно как-то выделить текущий раздел на фоне остальных:

    <xsl:template match="item" mode="global_menu">
    <xsl:choose>

    <xsl:when test="descendant-or-self::*/@id = $cur">
    <xsl:attribute name="id"><xsl:text>cur</xsl:text></xsl:attribute>

    <xsl:if test="descendant::*/@id = $cur">
    <xsl:call-template name="href_attribute"/>
    </xsl:if>
    <xsl:value-of select="title"/>

    </xsl:when>

    <xsl:otherwise>
    <xsl:call-template name="href_attribute"/>
    <xsl:value-of select="title"/>

    </xsl:otherwise>
    </xsl:choose>

    </xsl:template>


    Здесь мы сталкиваемся с новой конструкцией: <xsl:if test="descendant::*/@id = $cur"></xsl:if>

    Инструкция xsl:if позволяет реализовывать условие. В отличие от xsl:choose, можно обозначить шаблон только для случая, когда условие выполняется. В нашем случае, мы проверяем descendant::*/@id - совпадение id дочерних элементов с переменной $cur.

    Для полноты нашего меню нам нужно где-то вывести подразделы (если таковые имеются) для выбранного раздела.

    Для простоты, выведем дерево всех подразделов текущего раздела в правой колонке нашего шаблона. Для этого откроем xsl/template.xsl и немного изменим шаблон <xsl:template match="content">:

    <xsl:template match="content">

    <xsl:apply-templates select="item[@container = 1]" mode="html"/>

    <xsl:if test="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]/item/@id">
    <xsl:apply-templates select="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]" mode="sub-menu">
    <xsl:with-param name="cur"><xsl:value-of select="//navigation/sections/@hit_id"/></xsl:with-param>
    </xsl:apply-templates>

    </xsl:if>

    <xsl:apply-templates select="item[@container = 2]" mode="html"/>

    </xsl:template>


    Добавили проверку существования дочерних элементов у текущего раздела:
    <xsl:if test="//navigation/sections/item[descendant-or-self::*/@id = //navigation/sections/@hit_id]/item/@id">

    При этом учитывается то обстоятельство, что текущим может быть подраздел раздела.

    Далее, нам нужно создать вызываемый шаблон: <xsl:apply-templates select="item" mode="sub-menu">

    Для этого, добавить следующий код в xsl/my_template/navigation.xsl:

    <xsl:template match="item" mode="sub-menu">

    </xsl:template>


    Обратите внимание, что в отличии от основного меню мы начинаем обработку не с элемента а с элемента , у которого есть дочерние элементы. В остальном, данный шаблон почти полностью повторяет шаблон основного меню.

    Такой универсальности позволяет добиться CSS и верстка с помощью слоев. Я до сих пор в отдельных случаях использую таблицы для верстки, но постепенно стараюсь переходить на DIV-верстку, т.к. она позволяет значительно сократить код и, что более важно для XSLT, сделать шаблоны универсальными. Например, для большей части последних сайтов мне не пришлось переделывать navigation.xsl.

    Однако, отличия в подменю все-же есть. В том, что оно может иметь много уровней. Соответственно, шаблон <xsl:apply-templates select="item" mode="sub-menu-items">
    также будет отличаться от соответствующего шаблона для основного меню разделов:

    <xsl:template match="item" mode="sub-menu-items">
    <xsl:choose>

    <xsl:when test="descendant-or-self::*/@id = $cur">
    <xsl:attribute name="id"><xsl:text>cur</xsl:text></xsl:attribute>

    <xsl:if test="descendant::*/@id = $cur">
    <xsl:call-template name="href_attribute"/>
    </xsl:if>
    <xsl:value-of select="title"/>
    </xsl:when>

    <xsl:otherwise>
    <xsl:call-template name="href_attribute"/>
    <xsl:value-of select="title"/>

    </xsl:otherwise>
    </xsl:choose>

    <xsl:if test="item/@id">

    </xsl:if>

    </xsl:template>


    Отличие одно - мы обрабатываем дочерние элементы item для каждого item, рекурсивно вызывая шаблон <xsl:template match="item" mode="sub-menu-items">.

    Продолжение следует…

    P.S. Подготовка статьи отнимает массу времени. Однако, эта работа позволила мне взглянуть свежим взглядом на свои старые XSL-шаблоны.

    UPD: Исходники.
    К сожалению, дальше пришлось отказаться от использования браузера для компиляции XSLT.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 37
    • 0
      Внесите, пожалуйста, основной текст под кат.
      • 0
        проглючило Firefox... исправил. Но в карму уже насрали.
        • 0
          пасибо
          PS не я :)
          • 0
            я и не говорю. Обычно это происходит молча....
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Видимо, всю уже просрал *-)
        • 0
          Исходники выложу позже...
          • 0
            По поводу CDATA, порой встречается достаточно объемные куски кода, заключенные в вышеупомянутые <![CDATA[ код ]]>, имхо это делает код менее читаемым. Можно вставить в самом начале документа (после <xsl:stylesheet ...>), такую конструкцию:
            <xsl:output method="[xml][html][svg]" ... cdata-section-elements="tag"/>, где tag, все, что Вы не хотите анализировать, например script style br и т.д.
            • 0
              Предложение интересное. Однако, в данном случае в CDATA заключается материалы модулей: статьи, новости и сообщения форумов. Мне при, чтении XML-кода, нужно лишь знать где контент, а где XML-код документа. С CDATA это сделать проще, т.к. большинство редакторов XML (или поддерживающих синтаксис) блок CDATA выделяют, т.ч. его легко найти.

              Кроме того, все теги не предусмотреть.
              • 0
                А я бы воспользовался xsl:copy-of. Эта команда копирует кусок исходного дерева в результирующее.
              • +1
                CDATA - зло. Ибо она как раз позволяет пихать все что угодно в код, не задумываясь ни о чем. Использую только в крайних случаях, стараюсь избегать.

                З.Ы. Вы в XML запихиваете HTML иногда или я неправильно понял?
                • 0
                  Весь контент у меня в CMS редактируется через WYSIWYG-редактор и храниться в базе. При формировании XML-документа я блоки этого контента (ели я знаю, что они могут содержать пользовательское форматирование) помещаю в CDATA.
                  • 0
                    А в базе он хранится в виде html? По-хорошему можно настроить WYSIWYG так, чтобы он форматил в XML.
                    • 0
                      Это больная проблема. Я пробовал. Если у вас есть рабочее решение или пример - поделитесь.
                      • 0
                        Я озадачился поиском подобного, есть кое-что на примете. Если то что надо - скину.
                        • 0
                          В PHP есть отличная функция DomDocument->loadHTML($htmlString), которая лишает многих головных болей. Она легко справится с <br/> и <br> и закроет незакрытые теги. Она же конвертирует HTML в валидный XHTML который вы сможете хранить в XML-документе без CDATA (в CDATA попудут только участки CSS, JS и т.п.).
                          Так же можно будет вырезать все опасные для дизайна и пользователей участки: class, style, script и, как следствие, повысить безопасность…
                          • 0
                            Я пользовался подобными решениями (не в PHP), однако, от них пришлось отказаться, т.к.:
                            - пришлось иногда вставлять Flash, баннеры и другой специфический контент.
                            - вырезание кода часто упирается в человеческий фактор: пользователь может ввести такое форматирование, которое сложно очистить
                            - больше гемора.
                          • 0
                            http://www.wymeditor.org/
                            Это не WYSIWYG редактор.
                        • 0
                          Вполне возможно подобрать хороший «ви-зи-виг»-редактор, который бы не генерировал мусор, а так же выдавал XHTML или XML.
                          Всё включать в CDATA — не очень хорошо.

                          А, в общем, спасибо за интересный материал.
                          • 0
                            все правильно
                            просто перед сохранением преобразовывайте все в HTML сущности и тогда пусть юзер пихает туда что угодно и с CDATA нет проблем
                        • 0
                          в качестве бонуса - я пользуюсь xslt-шаблонами для генерации html-кода на своей машине, а результат - все html страницы заливаю на сайт по ftp.

                          Экономия на CMS =)
                          • 0
                            Я тоже иногда так делаю, но, в основном, когда нужно просто подготовить материал или сделать типографику текста.
                            • 0
                              А чем перегоняету, к примеру, готовый сайт со страницами арзложенными по разным папкам?
                              не поделитесь?
                              • 0
                                Своей CMS-кой. Было нужно только пару раз:
                                - сверстать код с типографикой
                                - залить на сервер все в статике, т.к. MySQL временно глючил, а клиенту нужно было все срочно показывать.
                          • 0
                            А подскажите, пожалуйста, XSL-редактор, желательно с подсветкой, комплитом и прочим, и желательно с процессором.

                            Решил написать себе XSL для перегонки майнмапов из FreeMind в Todo список.

                            Ваша статья очень вовремя (:
                            • 0
                              Altova XMLSpy можно попробывать... и подсветка кода есть и помошь при наборе и "компилить" можно
                              • 0
                                Я пользуюсь для XSL notepad++, а для работы с кодом CMS HomeSite. Я бы с удовольствием оставил один HomeSite, но последний плохо работает в кодировке UTF-8.
                                • 0
                                  Компил в редакторе я не использую. В XMLSpy, предложенном все есть. Он, кстати, рекомендован разработчиками XML как основной редактор.
                              • 0
                                Выложил исходники.
                                • 0
                                  Я конечно понимаю, что "К сожалению, дальше пришлось отказаться от использования браузера для компиляции XSLT."
                                  Но disable-output-escaping в FireFox не поддерживается.
                                  Решение у меня уже есть, но оно полностью не протестировано и не внедрено. Как только - так сразу наверно и сюда выложу.
                                  • 0
                                    Было бы очень кстати.
                                  • 0
                                    Извините, если не по адресу. Очень нужно быстренько создать свой сайт. Где можно найти бесплатные шаблоны?
                                  • 0
                                    Насчет компиляции XSLT на стороне сервера
                                    Подскажите, пожалуйста, как без гемороя генерить на сервере и отдавать браузеру уже готовый XHTML
                                    • +1
                                      Не совсем понятен вопрос. Что означает "без геммороя"? Если вопрос как, то:
                                      1. Об этом я и писал в этой и предыдущей статье

                                      2. Google
                                        - PHP: XSLT - Manual
                                        - Выполнение XSLT-преобразований в PHP
                                      Все зависит от конкретного языка. Я использую Parser и там также все просто
                                      • 0
                                        Спасибо! Последней ссылки раньше не видел. Думаю, это какраз то, что нужно.
                                        P.S. Поставил бы вам пару плюсов, еслиб мог.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.