免责声明:请注意这个问题的长度。对于一个现实世界的问题,这是一个反复出现的问题,我已经看到它被问了数百次,但从未提出过明确的、可行的解决方案。
我有数百个 HTML 文件,我想使用 PHP 批量缩进。起初我想使用 Tidy,但你应该知道,默认情况下它与 HTML5 标记和属性不兼容,经过一些研究和更多测试后,我想出了以下“假冒”HTML 5 支持的实现:
function Tidy5($string, $options = null, $encoding = 'utf8')
{
$tags = array();
$default = array
(
'anchor-as-name' => false,
'break-before-br' => true,
'char-encoding' => $encoding,
'decorate-inferred-ul' => false,
'doctype' => 'omit',
'drop-empty-paras' => false,
'drop-font-tags' => true,
'drop-proprietary-attributes' => false,
'force-output' => true,
'hide-comments' => false,
'indent' => true,
'indent-attributes' => false,
'indent-spaces' => 2,
'input-encoding' => $encoding,
'join-styles' => false,
'logical-emphasis' => false,
'merge-divs' => false,
'merge-spans' => false,
'new-blocklevel-tags' => ' article aside audio details dialog figcaption figure footer header hgroup menutidy nav section source summary track video',
'new-empty-tags' => 'command embed keygen source track wbr',
'new-inline-tags' => 'btidy canvas command data datalist embed itidy keygen mark meter output progress time wbr',
'newline' => 0,
'numeric-entities' => false,
'output-bom' => false,
'output-encoding' => $encoding,
'output-html' => true,
'preserve-entities' => true,
'quiet' => true,
'quote-ampersand' => true,
'quote-marks' => false,
'repeated-attributes' => 1,
'show-body-only' => true,
'show-warnings' => false,
'sort-attributes' => 1,
'tab-size' => 4,
'tidy-mark' => false,
'vertical-space' => true,
'wrap' => 0,
);
$doctype = $menu = null;
if ((strncasecmp($string, '<!DOCTYPE', 9) === 0) || (strncasecmp($string, '<html', 5) === 0))
{
$doctype = '<!DOCTYPE html>'; $options['show-body-only'] = false;
}
$options = (is_array($options) === true) ? array_merge($default, $options) : $default;
foreach (array('b', 'i', 'menu') as $tag)
{
if (strpos($string, '<' . $tag . ' ') !== false)
{
$tags[$tag] = array
(
'<' . $tag . ' ' => '<' . $tag . 'tidy ',
'</' . $tag . '>' => '</' . $tag . 'tidy>',
);
$string = str_replace(array_keys($tags[$tag]), $tags[$tag], $string);
}
}
$string = tidy_repair_string($string, $options, $encoding);
if (empty($string) !== true)
{
foreach ($tags as $tag)
{
$string = str_replace($tag, array_keys($tag), $string);
}
if (isset($doctype) === true)
{
$string = $doctype . "\n" . $string;
}
return $string;
}
return false;
}
它可以工作,但有 2 个缺陷:HTML 注释script
和style
标签没有正确缩进:
<link href="/_/style/form.css" rel="stylesheet" type="text/css"><!--[if lt IE 9]>
<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<!--<script type="text/javascript" src="//raw.github.com/kevinburke/tecate/master/tecate.js"></script>-->
</script><script charset="UTF-8" src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.0.0/js/locales/bootstrap-datepicker.pt.js" type="text/javascript">
</script><!--<script src="/3rd/parsley/i18n/messages.pt_br.js"></script>-->
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/parsley.js/1.1.10/parsley.min.js"></script>-->
<script src="/3rd/select2/locales/select2_locale_pt-PT.js" type="text/javascript">
</script><script src="/3rd/tcrosen/bootstrap-typeahead.js" type="text/javascript">
还有另一个更严重的缺陷:Tidy 将所有menu
标签转换为ul
并坚持删除任何空的内联标签,迫使我绕过它。为了清楚地说明这一点,这里有一些例子:
<br>
空标签<i>text</i>
内联标签<i class="icon-home"></i>
空的内联标签(来自 Font Awesome 的示例)
如果您检查代码,您会注意到我已经使用了不完美的b
hack来说明i
和menu
标记- 我本可以使用更强大的正则表达式,甚至可以完成相同的事情,但对于我的目的来说更快并且够好了。然而,这仍然留下了我没有考虑到的任何其他空的内联标签,这很糟糕。 str_replace
str_ireplace
str_replace
所以我转向DOMDocument
,但我很快发现为了formatOutput
工作,我必须:
- 去除标签之间的所有空格(当然使用正则表达式:
'~>[[:space:]]++<~m'
>><
) - 将所有换行符组合转换为例如
\n
它不会编码\r

- 将输入字符串加载为 HTML,输出为 XML
令我惊讶的是,DOMDocument 也存在空内联标签的问题,基本上,每当它看到<i class="icon-home"></i><someOtherTag>text</someOtherTag>
或类似的时候,它就会将其转为<i class="icon-home"><someOtherTag>text</someOtherTag></i>
完全打乱页面的浏览器呈现的问题。为了克服这个问题,我发现使用LIBXML_NOEMPTYTAG
withDOMDocument::saveXML()
会将任何没有内容的标签(包括真正的空标签,例如<br />
)变成内联结束标签,例如:
<i class="icon-home"></i>
保持不变(应该)<br>
变得<br></br>
混乱浏览器渲染(再次)
为了解决这个问题,我必须使用一个正则表达式来查找~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>~
匹配的字符串并将其替换为简单的/>
. 另一个主要问题saveXML()
是它在我的和内部的 HTML 周围添加了<![CDATA[
..]]>
块,这使得它们的内容无效,我必须再次返回这些标记。这“有效”:script
style
preg_replace
function DOM5($html)
{
$dom = new \DOMDocument();
if (libxml_use_internal_errors(true) === true)
{
libxml_clear_errors();
}
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
$html = preg_replace(array('~\R~u', '~>[[:space:]]++<~m'), array("\n", '><'), $html);
if ((empty($html) !== true) && ($dom->loadHTML($html) === true))
{
$dom->formatOutput = true;
if (($html = $dom->saveXML($dom->documentElement, LIBXML_NOEMPTYTAG)) !== false)
{
$regex = array
(
'~' . preg_quote('<![CDATA[', '~') . '~' => '',
'~' . preg_quote(']]>', '~') . '~' => '',
'~></(?:area|base(?:font)?|br|col|command|embed|frame|hr|img|input|keygen|link|meta|param|source|track|wbr)>~' => ' />',
);
return '<!DOCTYPE html>' . "\n" . preg_replace(array_keys($regex), $regex, $html);
}
}
return false;
}
似乎两种最受推荐和验证的 HTML 缩进方法并不能在野外为 HTML5 产生正确或可靠的结果,我不得不屈服于黑暗之神 Cthulhu。
我确实尝试过其他库,例如:
- html5lib - 无法
DOMDocument::$formatOutput
工作 - tidy-html5 - 和正常一样的问题
tidy
,除了它支持 HTML5 标签/属性
在这一点上,如果不存在更好的解决方案,我正在考虑编写仅适用于正则表达式的东西。但我认为也许DOMDocument
可以通过使用自定义 XSLT 来强制使用 HTML5 和script
/style
标记。我以前从未使用过 XSLT,所以我不知道这是否现实,也许你们中的一位 XML 专家可以告诉我,或许可以提供一个起点。