Правильная HTML-сериализация в .Net

    Доброго всем!

    Те, кто активно использует XSLT для генерации HTML (не XHTML), наверное часто сталкивались с ситуациями, когда необходимо генерировать не только валидный XML — XHTML, но и для браузеров, не поддерживающих XHTML, генерировать валидный HTML, что, вобщем-то, не одно и тоже. Для этого мы использовали «грязные хаки» в XSLT.
    В этой заметке я расскажу о более чистом и красивом методе, который, к сожалению, не часто используется.

    Метод специфичен для инфраструктуры .Net, но, вероятно, в других платформах есть схожие средства.

    Ну а теперь по порядку.

    Введение


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

    Суть проблемы


    Критичные отличия в сериализации XML от HTML достаточно просты:
    • Документ не может иметь xml-декларации;
    • Некоторые элементы должны иметь закрывающий тег, что означает, что стандартный XML-сериализатор неправильно сделает самозакрывающий тег <div/> для пустого div'а, т.к. HTML парсер должен ожидать закрывающий тег для div;
    • некоторые элементы не могут содержать ссылки на сущности, что означает, что HTML-парсер не обрабатывает ссылки на сущности в таких элементах как script или style.

    Помимо этого, есть ограничения, которые зависят от самого содержимого (сериализатор тут не причем), но важно, чтобы эти ограничения выполнялись:
    • Некоторые элементы не могут иметь содержимого, т.е. должны быть пустыми; например, не допускается содержимого для элемента link, и поэтому, если для link он даже не будет иметь содержимого, но будет наличествовать отдельный закрывающий тег, то это будет ошибкой HTML-парсера (которую, конечно, он проигнорирует);
    • некоторые элементы не могут содержать дочерних элементов или комментариев, это такие элементы как title и textarea;
    • общие ограничения структуры документа, которые здесь мы не будем рассматривать, а оставим пытливым умам =)

    Сам метод


    Дело в том, что для сериализации XML среда использует XmlWriter, который и берет на себя всю работу по правильному форматированию XML. Используется этот класс практически во всех операциях где нужно каким-то образом записывать XML. В частности, при XSL-трансформациях ( XslCompiledTransform.Transform), экземпляр данного класса используется в качестве места назначения.

    Так вот, и всего-то что нужно это реализовать свой XmlWriter, который и будет правильно форматировать наш XML в соответствии с правилами HTML. Итак, представляем — HtmlXmlWriter!

    Теория


    Берем спецификацию HTML, а конкретнее HTML5 (куда же сейчас без него), и видим, что в ней выделяются 5 типов элементов:
    • Void (пустые) элементы — area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source;
    • raw text (чисто-текстовые) элементы — script, style;
    • элементы RCDATA (только текст) — textarea, title;
    • foreign (внешние) элементы — любые внешние не HTML-элементы, в частности из MathML и SVG, но мы будем таковыми считать любые элементы не из пространства имен XHTML;
    • normal (обычные) элементы — все остальные HTML-элементы;

    Теперь наш HtmlXmlWriter должен контролировать и не позволять, чтобы в пустые (void) элементы не добавлялось никакого содержимого и они всегда были бы самозакрывающимися (<col/>).

    Чисто-текстовые (raw text) могут иметь только текст (никаких сущностей или комментариев), но не должен содержать последовательность, которая может быть истолкована как закрывающий тег (независимо от регистра).

    RCDATA не могут иметь дочерних элементов, а могут иметь только текст, включая ссылки на сущности. Комментарии в них тоже, вроде как нельзя.

    Внешние (foreign) элементы могут быть любыми — это обычный XML. Никаких ограничений.

    Нормальные (normal) элементы могут также содержать все что они захотят, но только нужен обязательно закрывающий тег.

    Реализация


    Ну, собственно, саму реализацию я здесь приводить не буду, она не сложна и каждый может сделать ее сам. Я себе сделал, и, возможно, когда задокументирую, и если меня будут слезно умолять, выложу на какое-нибудь кодохранилище. Здесь же приведу только полезные заметки (возможно немного сумбурные).

    HtmlXmlWriter будет наследником XmlWriter. Должен агрегировать сторонний экземпляр XmlWriter (который нужно в конструктор передать), и по-умолчанию вызывать соответствующие методы из него.

    HtmlXmlWriter должен следить на каком элементе он сейчас находится (имя и тип последнего элемента), определяя это в методе XmlWriter.WriteStartElement/WriteEndElement. Также должен следить находится ли он на атрибуте (WriteStartAttribute/WriteEndAttribute).

    При закрытии элемента (WriteEndElement/WriteFullEndElement), выбирать в зависимости от типа элемента использовать WriteEndElement или WriteFullEndElement.

    Самое сложное с raw text элементами, потому как XmlWriter будет экранировать некоторые символы. Поэтому нужно на них подменять вывод текста (WriteCharEntity, WriteString, WriteSurrogateCharEntity) на WriteRaw. Но тут надо не забыть контролировать, чтобы в тексте небыло закрывающего тега.

    Заключение


    Теперь имея такой класс, можно запросто передавать его в XSL-трансформации (или куда еще) и получать нормальный HTML из XHTML, так что это будет понимать даже любой тупой HTML-парсер.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      +3
      Вообще-то все это делает стандартными средствами XSLT — www.w3schools.com/xsl/el_output.asp

      Поэтому при надобности вывода в HTML, можно просто пропустить документ через маленький шаблончик.
        0
        Дело в том, что иногда (очень часто) нужно использовать один и тот же шаблон для генерации и XHTML и HTML. Более того, XSLT в .Net не умеет правильно сериализовывать в HTML при установки метода вывода в html. Там есть труднообходимые особенности. И, конечно же, это уже не будет HTML5.
          0
          Ну так наложите для генерации HTML один маленький шаблончик, задачей которого будет только преобразование XML -> HTML.

          Что значит «XSLT в .Net не умеет»? Есть разные парсеры. Тот же самый Saxon есть и для Java, и для .Net. С ним у меня проблем сериалайза в HTML не было.

          Может, вы просто плохой парсер используете? Ну так это не повод плодить извращения.

          Если же у вас проблемы с Saxon'ом, то будет интересно послушать, т.к. я с ним сам работаю.
            0
            А я не хочу saxon. Есть стандартный xslt из .net, который отлично справляется. А небольшие неровности его работы вполне себе исправляются. Но, в любом случае, спасибо. Ценю ваше высказанное мнение.
              0
              Стандартный XSLT в .Net — это устаревшее убожество. В свое время пришлось использовать AltovaXML через COM-обертку чтобы получить вменяемый XSLT в своем приложении. А ведь еще поддержку XQuery обещали… э-эх.
                0
                Да, к сожалению, XQuery еще в 2-м фреймворке обещали но потом убрали… жаль.
                Но, я бы не стал .Net xslt-процессор так называть — нужно провести реальные замеры производительности чтобы как-то сравнивать.
                0
                Правильно, зачем использовать лучший XSLT процессор, когда можно написать кучу костылей?..

                В той же Java Saxon умеет эмулировать стандартные интерфейсы XSLT преобразований, поэтому его внедрение происходит очень быстро и просто. Думаю и в .Net тоже самое.
                  0
                  Какой процессор лучше — это предмет для обсуждения, который данной статьей не поднимался, поэтому и здесь я бы не хотел это обсуждать. У меня есть свое мнение на счет Saxon, которое я также здесь не хочу говорить.
                  Суть в том, что здесь представлено решение конкретной задачи — использование XmlWriter для форматирования в соответствии с правилами HTML. Что его можно использовать для XSL-трансформаций — это вопрос другой.

                  Кстати, и все-таки, разве в Saxon можно использовать один и тот же шаблон для генерации XHTML и HTML, без использования хаков в XSLT?
                    0
                    Назовите любой другой XSLT процессор, поддерживающий хотя бы сравнимый набор функциональностей. Достаточно хотя бы того, что процессоров, поддерживающих XSLT 2.0, можно пересчитать по пальцам одной руки

                    Во-первых, в XSLT 2.0 есть тег <xsl:result-document>.
                    Во-вторых, ваша задача решается так: генерируется XHTML, потом на него накладывается ЕЩЕ ОДНО XSLT преобразование, которое делает HTML вывод.
                      0
                      То, что можно наложить еще одно преобразование — сомнений нет, но зачем это делать, если можно просто заменить сериализатор.

                      Вот взять опять же html5, документ которого описывается некой объектной моделью, которая укладывается в xml. Согласно спецификации она может быть сериализована либо в xhtml (по правилам xml) либо в html (по правилам, которые я привел в статье). Следовательно на входе должна быть объектная модель xml (пусть это бедет XmlDocument, XDocument, XPathNavigator или результат трансформаций) а на выходе текстовое представление в одном из форматов. И задача получить правильное представление ни какого-то дополнительного преобразования, а сериализатора.

                      А то, что вы предлагаете сделать, это дополнительная ненужная работа — это не кошерно =)

                      Без обид — мы просто, видимо, про разное говорим.
                        0
                        Какая разница как получать HTML строку — сериализатором или еще одним XSLT преобразованием? Только во втором случае мы имеем решение из коробки.

                        Вам в любом случае надо будет выводить строку. По сути — выводить результат XSLT преобразования. Будут две цепочки действий:
                        1. Сделали XHTML, вывели как строку
                        2. Сделали XHTML, преобразовали в HTML, вывели как строку

                        Зачем что-то заменять и писать велосипеды, когда есть хорошее готовое решение? Я чего-то не понимаю? Может, ваш вариант лучше, надежнее, быстрее для понимания другими разработчиками?

                        Дополнительная и ненужная работа у вас. Ибо нормальный разработчик, который знает про возможности XSLT, сделал бы все за 5 минут при помощи лишнего XSLT преобразования. Вы же слабо знаете возможности самого XSLT и XSLT процессоров, но, тем не менее, пишите статьи «правильная ...» (если что, я готов признать свою неправоту)

                        Короче, разговор ни о чем. Покажите, пожалуйста, чем ваше решение лучше мною предложенного на конкретных примерах.

                        ЗЫ: Я не пишу и никогда не писал на .Net. Так что чем отличается XmlDocument от XDocument не знаю.
                          0
                          Видимо вы опять не про то. Я не пишу про то какое решение лучше или хуже для XSLT. Я пишу о том как XHTML представление сериализовывать по HTML-правилам.
                          Заметьте, это ваши слова: «при помощи лишнего XSLT преобразования». Зачем нужно делать преобразование вообще? Просто есть другие правила форматирования — это не какая-то другая структура. Я не спорю, что можно сделать это на XSLT, но это просто глупо.

                          А ваше утверждение про мои знания XSLT и процессоров безосновательны. Я здесь не обсуждаю вопросы самого XSLT. Здесь решение задачи вышестоящего уровня.

                          Я не буду сомневаться в ваших знаниях XSLT, потому как видно, что это единственное слово, которое вы поняли, и не можете понять что статья не он XSLT.
            –1
            перенесите плиз в блог .NET, посты из личного блога не попадают на главную Хабра
              0
              кармы не хватало, спасибо за подкинутую — перенес.
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Неправильно.
                  script не может содержать ссылок на сущности. Это raw-text элемент.
                  Правильный HTML-парсер должен не распознавать ссылки на сущности.
                  Т.е. в данном случае, это будет трактоваться не как текст «google-analytics-config-compiled.js», а как текст "&google-analytics-config;".
                  Возможно, вы имели в виду:
                  <script type="text/javascript" src="&google-analytics-config;"></script>
                  если так, то — да, так можно. Но это атрибут.
                  0
                  Насколько понял из этого объявления сущности, его разбором будет заниматься XML-парсер на входе в XSLT, что совсем не то, о чем я писал. Здесь имеется в виду запись выходного потока XML по правилам HTML.

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

                Самое читаемое