Один из самых неприятных моментов в программировании сайтов, использующих XML, — это обработка кода, пришедшего от визуального редактора.
С одной стороны хочется разрешить коллегам-редакторам не только пользоваться предопределенными действиями, нажимая на кнопки, но и дать возможность ввести нужные HTML-теги, если это необходимо. Но при этом любой некорректный ввод, если его поместить в XML, потенциально способен не только сделать XML невалидным, но в худшем случае сделает неработоспособной и саму страницу с редакторским веб-интерфейсом, лишая возможности исправить ошибку.
Прямолинейное решение — экранировать все угловые скобки, обрамляющие теги, амперсанды и HTML-сущности, а потом обернуть все это в блок CDATA — ненамного лучше полного вырезания тегов, ибо приводит, среди прочего, к тому, что полученный код неудобен для обработки с помощью XSLT. Например, довольно сложно (и даже неоправданно сложно) будет дополнить внешние ссылки классом external:
Читайте об этом на <a href=»http://wikipedia.org/»>Википедии</a>.
Разумеется, возможно решить задачу, привлекая Perl, но чем сильнее разметка превращается в текст, тем сложнее обрабатывать более сложные случаи.
Все, что нужно сделать, — заставить себя один раз написать валидатор и затем им с удовольствием пользоваться.
Я использую примерно такой вариант. Полученный от пользователя код встраивается в итоговоый XML-документ так, словно он был написан участниками комитета W3C в редакторе Altova XML Spy:
my $xmlParser = new XML::LibXML(); $xmlParser->expand_entities(0); my $contentFragment = $xmlParser->parse_xml_chunk(make_well_formed_xml($content)); $contentNode->appendChild($contentFragment);
Функция make_well_formed_xml() выполняет несколько последовательных преобразований, включая правку с помощью libtidy и замену сущностей:
sub make_well_formed_xml { my $content = shift; utf8::decode($content); my $tidy = HTML::Tidy->new({ 'output-xhtml' => 1, 'doctype' => 'omit', 'show-body-only' => 1, 'fix-uri' => 1, }); $content = $tidy->clean($content); $content =~ s{<}{<!ENTITY lt>}g; $content = XML::Entities::decode('all', $content); $content =~ s{<!ENTITY lt>}{<}g; $content =~ s{&}{&}g; return $content; }
Грязные места в коде — декодирование UTF-8 и замену открывающей угловой скобки — конечно нужно заменить более аккуратным кодом, но поскольку все теперь локализовано в единой функции, это можно отложить на потом, и такой код будет работать годами 🙂
Отдельно нужно заменить, что правильная настройка libtidy весьма неочевидна, но разобраться в настройках помогут, например, онлайновые интерфейсы.