如何将PHP的DOMDocument与JavaScript模板相结合


How to combine PHP's DOMDocument with a JavaScript template

我这里有一个奇怪的问题,但它完全难倒了我。无论如何,这是因为我想不出要搜索的正确术语,所以这个问题很可能会在 StackOverflow 的某个地方得到回答,但我找不到它。

我们

有一个校对系统,允许我们获取页面并对其进行注释。我们可以将页面发送给我们的客户,他们可以在发送之前对其进行注释。在大多数情况下,这工作正常。当我们尝试使用类似于Handlebars的JavaScript模板系统时,问题就来了。我们倾向于在我们的页面上有如下所示的脚本模板:

<script type="client/template" id="foo-div">
<div>#foo#</div>
</script>

我们可以在脚本中使用它来生成模板中的标记,将#foo#替换为正确的数据。

当我们试图将其放入我们的校对系统时,问题就来了。因为我们需要抓取页面,以便我们可以在我们的域中呈现,所以我们使用 PHP 的DOMDocument来解析 HTML,以便我们可以轻松修改它(将target="_blank"等内容添加到外部链接等)。当我们尝试通过 DOMDocument 运行模板时,它会奇怪地解析它(可能将其视为无效的 XML),这会导致页面上出现问题。为了更好地说明这一点,下面是 PHP 中的一个例子:

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$html = '<!DOCTYPE html>'.
    '<html>'.
    '<head></head>'.
    '<body>'.
    '<script type="client/template" id="foo-div"><div>#foo#</div></script>'.
    '</body>'.
    '</html>';
$dom = new DOMDocument();
libxml_use_internal_errors(true);
try {
    $html = $dom->loadHTML($html);
} catch (Exception $e) {
    throw new Exception('Invalid HTML on the page has caused a parsing error');
}
if ($html === false) {
    throw new Exception('Unable to properly parse page');
}
$dom->preserveWhiteSpace = false;
$dom->formatOutput = false;
echo $dom->saveHTML();

此脚本生成类似于下面的 HTML 的代码,并且似乎不会引发任何异常。

<!DOCTYPE html>
<html>
<head></head>
<body><script type="client/template" id="foo-div"><div>#foo#</script></body>
</html>

我的问题是:有没有人知道我可以让 PHP 的DOMDocument不带模板script标签的方法?是否有一个设置或插件可以用来DOMDocument像浏览器一样以纯文本形式查看具有type属性的 script 标签的内容?

编辑

我最终选择了 Alf Eaton 的解决方案或将字符串解析为 XML。但是,并非所有 HTML 标记都是自闭合的,这会导致问题。我在这里发布完整的解决方案,以防有人遇到同样的问题:

/**
 * Inserts a new string into an old string at the specified position.
 * 
 * @param string $old_string Old string to modify.
 * @param string $new_string New string to insert.
 * @param int $position Position at which the new string should be inserted.
 * @return string Old string with new string inserted.
 * @see http://stackoverflow.com/questions/8251426/insert-string-at-specified-position
 */
function str_insert($old_string, $new_string, $position) {
    return substr($old_string, 0, $position) . $new_string .
        substr($old_string, $position);
}
/**
 * Inspects a string of HTML and closes any tags that need self-closing in order
 * to make the HTML valid XML.
 * 
 * @param string $html Raw HTML (potentially invalid XML)
 * @return string Original HTML with self-closing slashes added.
 */
function self_close($html) {
    $fixed = $html;
    $tags  = array('area', 'base', 'basefont', 'br', 'col', 'frame',
        'hr', 'img', 'input', 'link', 'meta', 'param');
    foreach ($tags as $tag) {
        $offset = 0;
        while (($offset = strpos($fixed, '<' . $tag, $offset)) !== false) {
            if (($close = strpos($fixed, '>', $offset)) !== false &&
                    $fixed[$close - 1] !== '/') {
                $fixed = str_insert($fixed, '/', $close);
            }
            $offset += 1; // Prevent infinite loops
        }
    }
    return $fixed;
}
// When parsing the original string:
$html = $dom->loadXML(self_close($html));

如果输入文档是有效的 XML,则将其解析为 XML 而不是 HTML 将保留<script>标记的内容:

<?php
$html = <<<END
<!DOCTYPE html>
<html><body>
<script type="client/template" id="foo-div"><div>#foo#</div></script>
</body></html>
END;
$doc = new DOMDocument();
$doc->preserveWhiteSpace = true; // needs to be before loading, to have any effect
$doc->loadXML($html);
$doc->formatOutput = false;
print $doc->saveHTML();
// <!DOCTYPE html>
// <html><body>
// <script type="client/template" id="foo-div"><div>#foo#</div></script>
// </body></html>

当PHP的DOMDocument解析HTML时,它使用了一些故障安全技术。
在脚本标记的情况下,有两个。

首先是特殊的 cript-tag 内容处理 - 因为<script>标签不能包含任何其他标签,它里面的所有内容都假定为文本。

第二种技术是整个html标签自动关闭黑客。当解析器找到节奏错误的结束标签时,它会尝试查找最近的父开始标签,并自动关闭此找到的开放标签和错误放置的结束标签之间的每个标签。如果解析器找不到正确的开放标签,它只会忽略关闭标签。

如果您尝试解析这样的代码,您可以看到这一点 <body><div><script type="client/template" id="foo-div"><div>#foo#</div>dfdf</script></div></body> - 您将<body><div><script type="client/template" id="foo-div"><div>#foo#</script></div>dfdf</body>脚本。

没有正常的方法可以让 DOMDocument 以您想要的方式解析 html5。
但是您可以使用一个简单的技巧 - 只需用正则表达式替换&lt; <的所有开角括号,或者仅替换脚本标签中任何其他未使用的符号。处理后,您可以通过相同的程序全部取回。