Pull to refresh

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

Reading time4 min
Views2.4K
Доброго всем!

Те, кто активно использует 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/&gt).

Чисто-текстовые (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-парсер.
Tags:
Hubs:
Total votes 32: ↑20 and ↓12+8
Comments18

Articles