В статье "Избавляемся о ошибок xHTML-валидации при использовании JavaScript, Flash, <noindex>, CSS" рассказывается о четырёх основных источниках неправильной разметки в WordPress:
- JavaScript;
- Глобальные CSS, размещаемые в заголовке документа;
- Flash;
- Несуществующий по стандартам тег <noindex>, придуманный Яндексом
Естественно, после прочтения статьи я решил проверить свой, как я полагал, валидный, блог. В том-то и дело, что только плагал: нашёлся пятый источник неправильной разметки. Где бы Вы думали? В самом WordPress, в функции wpautop()
.
Сначала всё же пройдусь по первым четырём источникам. А точнее, трём — проблему с Яндексом я для себя давно уже решил, а именно: несуществующими тэгами я не балуюсь. И не могу не удержаться от камня в огород Яндекс: ну зачем нужно было придумывать абсолютно левый тэг, когда можно было использовать, например, комментарий:
Content not to be indexed
<!-- /NOINDEX -->
Во-первых, это решает проблему невалидного (X)HTML. Во-вторых, не надо было бы заботиться о том, где можно и где нельзя размещать тэг — вырезал регэкспом такой вот комментарий от начала и до конца и получил контент, который можно индексировать. Эх… Ладно, забыли.
Теперь по списку.
JavaScript. Решение простое: весь JavaScript заключаем в секцию CDATA
. Например, так:
<!--//--><![CDATA[//><!--
//JavaScript goes here...
//--><!]]>
</script>
И не забываем, что атрибут type
— обязательный. Решение выше, что называется, для любителей поизвращаться: помимо всего прочего, оно прячет JavaScript от антикварных браузеров, которые вообще не понимают тэг script
. Если поддержка таких браузеров не нужна, то код можно значительно упростить:
//JavaScript goes here...
/*]]>*/
</script>
А вот такое решение я не рекомендую:
//JavaScript goes here...
//-->
</script>
Причина простая: если мы планируем отдавать XHTML так, как это требует W3C (с MIME-типом application/xhtml+xml), то любой нормальный браузер воспримет HTML-комментарий как элемент, вложенный в script
, и, как следствие, проигнорирует скрипт полностью (в XHTML парсер не знает о том, что существуют скрипты, таблицы стилей и прочие вещи).
С CSS всё аналогично. Метод для любителей поизвращаться:
<!--/*--><![CDATA[/*><!--*/
/* CSS goes here*/
/*]]>*/-->
</style>
И более простой метод:
/*<![CDATA[*/
/* CSS goes here*/
/*]]>*/
</style>
Да, у тэга style
атрибут type
тоже является обязательным.
Все браузеры, появившиеся после 2000 года понимают форму "без извращений", поэтому, наверное, имеет смысл использовать именно её. И еще небольшое замечание по поводу CDATA
. По большому счету, секция CDATA
нужна только тогда, когда контент содержит символы, которые могут быть интерпретированы как разметка XML, а именно: < и &. То есть в большинстве случаев, стили внутри тэга style
помещать в CDATA
особо не нужно.
Третий пункт — Flash. Здесь поможет Flash Satay. На нём подробно останавливаться не буду, лишь замечу, что правильный способ записи такого монстра:
<param name="movie" value="movie.swf"/>
<embed src="movie.swf" quality="high" width="400" height="300" name="movie" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"/>
</object>
выглядит так:
<param name="movie" value="movie.swf"/>
</object>
Достоинства и недостатки метода подробно описаны в оригинале, нет смысла на них останавливаться здесь.
По четвёртому вопросу я уже высказывался. У Дмитрия в статье есть решение, но лично для себя я его не считаю приемлемым.
Переходим к пятому пункту, ради которого, собственно говоря, данная статья и писалась Как я уже сказал, во многих случаях в неправильной разметке виноват WordPress. Конкретно — функция
wpautop()
. Я всегда считал, что глобально менять разметку HTML, использую регулярные выражения — глупо, ибо в общем случае это работать не будет.
Допустим, Вы привыкли использовать хорошо структурированый код. Тогда вполне вероятно, что Вы можете написать нечто подобное (в целях упрощения восприятия я сократил код):
<strong>Test</strong>
<div>Lorem ipsum dolor sit amet, иными словами, вложенный div.</div>
</div>
Дальнейшие рассуждения исходят из предположения, что Вы не пользуетесь WYSIWYG-редактором. Например, из-за того, что приходится вставлять разметку, которую TinyMCE никак не переваривает (исправляет её под себя), я им вообще не пользуюсь, предпочитая обыкновенный HTML-редактор без наворотов.
В этом случае функция wpautop()
преобразует его следующим образом:
- Добавление символов перевода строки:
[-]View Code HTML<div><strong>Test</strong>
<div>Lorem ipsum dolor sit amet, иными словами, вложенный div.</div>
</div> - Добавление параграфов куда можно и куда нельзя:
[-]View Code HTML<p><div><strong>Test</strong></p>
<p><div>Lorem ipsum dolor sit amet, иными словами, вложенный div.</div></p>
<p></div></p> - Финальная очистка:
[-]View Code HTML<div><strong>Test</strong></p>
<div>Lorem ipsum dolor sit amet, иными словами, вложенный div.</div>
</div>
Как видим, остается лишний закрывающий тэг p
. Кстати, если написать так (строчный и блочный элементы на одной строке):
<strong>Test</strong><div>Lorem ipsum dolor sit amet, иными словами, вложенный div.</div>
</div>
то разметка будет нормальной.
В общем случае, ошибка вылезает при такой разметке:
<inlinetag></inlinetag>
<blocktag></blocktag>
</blocktag>
Править все статьи — абсолютно не метод русского программиста Проще автоматизировать.
Открываем файл wp-includes/formatting.php
:
- function wpautop($pee, $br = 1) {
- $pee = $pee . "\n"; // just to make things a little easier, pad the end
- $pee = preg_replace('|<br />\s*<br />|', "\n\n", $pee);
- // Space things out a little
- $allblocks = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|map|area|blockquote|address|math|style|input|p|h[1-6]|hr)';
- $pee = preg_replace('!(<' . $allblocks . '[^>]*>)!', "\n$1", $pee);
- $pee = preg_replace('!(</' . $allblocks . '>)!', "$1\n\n", $pee);
- $pee = str_replace(array("\r\n", "\r"), "\n", $pee); // cross-platform newlines
- if ( strpos($pee, '<object') !== false ) {
- $pee = preg_replace('|\s*<param([^>]*)>\s*|', "<param$1>", $pee); // no pee inside object/embed
- $pee = preg_replace('|\s*</embed>\s*|', '</embed>', $pee);
- }
- $pee = preg_replace("/\n\n+/", "\n\n", $pee); // take care of duplicates
- $pee = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "<p>$1</p>\n", $pee); // make paragraphs, including one at the end
- $pee = preg_replace('|<p>\s*?</p>|', '', $pee); // under certain strange conditions it could create a P of entirely whitespace
- $pee = preg_replace('!<p>([^<]+)\s*?(</(?:div|address|form)[^>]*>)!', "<p>$1</p>$2", $pee);
- $pee = preg_replace( '|<p>|', "$1<p>", $pee );
- $pee = preg_replace('!<p>\s*(</?' . $allblocks . '[^>]*>)\s*</p>!', "$1", $pee); // don't pee all over a tag
- $pee = preg_replace("|<p>(<li.+?)</p>|", "$1", $pee); // problem with nested lists
- $pee = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $pee);
- $pee = str_replace('</blockquote></p>', '</p></blockquote>', $pee);
- $pee = preg_replace('!<p>\s*(</?' . $allblocks . '[^>]*>)!', "$1", $pee);
- $pee = preg_replace('!(</?' . $allblocks . '[^>]*>)\s*</p>!', "$1", $pee);
- if ($br) {
- $pee = preg_replace_callback('/<(script|style).*?<\/\\1>/s', create_function('$matches', 'return str_replace("\n", "<WPPreserveNewline />", $matches[0]);'), $pee);
- $pee = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $pee); // optionally make line breaks
- $pee = str_replace('<WPPreserveNewline />', "\n", $pee);
- }
- $pee = preg_replace('!(</?' . $allblocks . '[^>]*>)\s*<br />!', "$1", $pee);
- $pee = preg_replace('!<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)!', '$1', $pee);
- if (strpos($pee, '<pre') !== false)
- $pee = preg_replace_callback('!(<pre.*?>)(.*?)<\/pre>!is', 'clean_pre', $pee );
- $pee = preg_replace( "|\n</p>$|", '</p>', $pee );
- $pee = preg_replace('/<p>\s*?(' . get_shortcode_regex() . ')\s*<\/p>/s', '$1', $pee); // don't auto-p wrap shortcodes that stand alone
- return $pee;
- }
После строки 77 вставляем:
- $pee = preg_replace("/<p>\\s*(<{$allblocks}.*?)<\\/p>/ism", '$1', $pee);
Эта строка кода убирает параграф вокруг открывающего тэга блочного элемента. Такой простой вот трюк (я на него убил минут 30 отладки) решает проблему с форматированием.
Но, как оказалось, это еще не всё. Второй случай более редкий:
<div></div>
text</li>
Что характерно, перед открывающим <li> должен стоять по крайней мере пробел (редактор WordPress его добавляет, так что условие вполне выполнимое).
Так выглядит результат работы неизменённого wpautop()
:
<div></div>
<p>text</li>
А так — исправленного:
<div></div>
<p>text</li>
В исправленном варианте на одну ошибку меньше Но всё равно нужно исправлять.
Здесь лечение более сложное: во-первых, нужно добавить одну функцию:
{
if ('</p>' == $matches[2]) {
return '<p>' . $matches[1] . $matches[2];
}
return $matches[1] . $matches[2];
}
и в функцию wpautop()
под строку
добавить
После этого вроде всё хорошо. За исключением элементов <ins>
и <del>
. Так как они могут иметь как блочный, так и строчный контекст, решение я пока не придумал. Единственное, что я могу посоветовать: если нужен <ins>
или <del>
в блочном котексте, его нудно поместить в <div>
:
whatever
</ins></div>
По традиции, патч в формате unified diff, который исправляет проблему:
+++ formatting.php 2008-07-07 05:44:52.000000000 +0300
@@ -59,6 +59,15 @@
return $text;
}
+function wpautop_replace_callback($matches)
+{
+ if ('</p>' == $matches[2]) {
+ return '<p>' . $matches[1] . $matches[2];
+ }
+
+ return $matches[1] . $matches[2];
+}
+
function wpautop($pee, $br = 1) {
$pee = $pee . "\n"; // just to make things a little easier, pad the end
$pee = preg_replace('|<br />\s*<br />|', "\n\n", $pee);
@@ -75,11 +84,13 @@
$pee = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "<p>$1</p>\n", $pee); // make paragraphs, including one at the end
$pee = preg_replace('|<p>\s*?</p>|', '', $pee); // under certain strange conditions it could create a P of entirely whitespace
$pee = preg_replace('!<p>([^<]+)\s*?(</(?:div|address|form)[^>]*>)!', "<p>$1</p>$2", $pee);
+ $pee = preg_replace("/<p>\\s*(<{$allblocks}.*?)<\\/p>/ism", '$1', $pee);
$pee = preg_replace( '|<p>|', "$1<p>", $pee );
$pee = preg_replace('!<p>\s*(</?' . $allblocks . '[^>]*>)\s*</p>!', "$1", $pee); // don't pee all over a tag
$pee = preg_replace("|<p>(<li.+?)</p>|", "$1", $pee); // problem with nested lists
$pee = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $pee);
$pee = str_replace('</blockquote></p>', '</p></blockquote>', $pee);
+ $pee = preg_replace_callback("/<p>(.*?)(<\\/{$allblocks}>)/s", 'wpautop_replace_callback', $pee);
$pee = preg_replace('!<p>\s*(</?' . $allblocks . '[^>]*>)!', "$1", $pee);
$pee = preg_replace('!(</?' . $allblocks . '[^>]*>)\s*</p>!', "$1", $pee);
if ($br) {
Да, жаль, конечно, что разработчики WordPress не позаботились о данных моментах. Однако, для меня такой проблемы, в общем-то, и не существует, т.к. я использую структуру кода, которую WordPress не ломает
А я просто проверяю код в редакторе, а за идею спасибо.
Та же проблема с , если хоть сколько отбивать переводами строки после текст, написанный в HTML (а не визуальном), то он также забудет поставить в начале. Вообще, решать это надо не исправлением wautop, а добавлением фильтра с приоритетом ниже.
Я для себя решил эту проблему плагином, который самостоятельно убирает фильтр
wpautop()
и устанавливает (с тем же приоритетом) замену.Прочитать можно в статье FormatControl — плагин для решения проблем с форматированием в WordPress.
О, сейчас посмотрим, что там. Ничего что добавлю исправление обработки из твоего плагина в свой типограф? С указанием, конечно.
Без проблем.
Полезная заметка про noindex.