Один из самых неприятных моментов в программировании сайтов, использующих 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 весьма неочевидна, но разобраться в настройках помогут, например, онлайновые интерфейсы.