在HTML中标记文本

3
我有一些纯文本和HTML,需要创建一个PHP方法,该方法将返回相同的HTML,但在任何文本实例之前加上<span class="marked">,并在其后添加</span>
请注意,它应支持html中的标签(例如,如果文本是blabla,则当bla<b>bla</b><a href="http://abc.com">bla</a>bla时,应进行标记。
它不区分大小写,并支持长文本(包括多行等)。
例如,如果我使用文本“my name is josh”和以下HTML调用此函数:
<html>
<head>
    <title>My Name Is Josh!!!</title>
</head>
<body>
    <h1>my name is <b>josh</b></h1>
    <div>
        <a href="http://www.names.com">my name</a> is josh
    </div>

    <u>my</u> <i>name</i> <b>is</b> <span style="font-family: Tahoma;">Josh</span>.
</body>
</html>

... it should return:

<html>
<head>
    <title><span class="marked">My Name Is Josh</span>!!!</title>
</head>
<body>
    <h1><span class="marked">my name is <b>josh</b></span></h1>
    <div>
        <span class="marked"><a href="http://www.names.com">my name</a> is josh</span>
    </div>

    <span class="marked"><u>my</u> <i>name</i> <b>is</b> <span style="font-family: Tahoma;">Josh</span></span>.
</body>
</html>

感谢您的选择。

5
不能在<title>标签内使用<span>标签。 - kennytm
4
我会尽力进行翻译。以下是需要翻译的内容:@Kenny - How else am I supposed to make my site's tab have a 48px font? - Nick Craver
那么像<b><u>这样的内联元素被认为是“文本的实例”吗? - Richard JP Le Guen
@Richard JP Le Guen:我猜.. - Alon Gubkin
5个回答

12
这会有些棘手。虽然你可以通过简单的正则表达式来实现,忽略标签内的任何内容,但类似以下的天真方法并不可取:
preg_replace(
    'My(<[^>]>)*\s+(<[^>]>)*name(<[^>]>)*\s+(<[^>]>)*is(<[^>]>)*\s+(<[^>]>)*Josh',
    '<span class="marked">$0</span>', $html
)

这并不是完全可靠的。部分原因在于HTML不能通过正则表达式进行解析:在属性值中放置>是有效的,其他非元素结构,如注释,也会被错误解析。即使使用更严格的表达式来匹配标记 - 类似于<[^>\s]*(\s+([^>\s]+(\s*=\s*([^"'\s>][\s>]*|"[^"]*"|'[^']*')\s*))?)*\s*\/?>这样极难操作的东西 - 您仍然会面临许多相同的问题,特别是如果输入的HTML未经验证。

这甚至可能成为一个安全问题,因为如果您处理的HTML不可信,则可能会欺骗您的解析器将文本内容转换为属性,从而导致脚本注入。

但是,即使忽略这一点,您也无法确保正确的元素嵌套。所以您可能会将:

<em>My name is <strong>Josh</strong>!!!</em>

混乱和无效的嵌套:

<span class="marked"><em>My name is <strong>Josh</strong></span>!!!</em>

或者:

My
<table><tr><td>name is</td></tr></table>
Josh

当那些元素无法用标签包裹时,您就需要进行处理了。如果您运气不佳,浏览器的修复机制可能会导致整个页面被“标记”,或者破坏页面布局。

因此,您需要在解析DOM时进行处理,而不是通过字符串操作。您可以使用PHP解析整个字符串,对其进行处理并重新序列化,但如果从辅助功能的角度来看是可接受的话,那么在JavaScript中处理起来可能更容易,因为内容已经解析为DOM节点。

然而,这仍然会很困难。这个问题处理的情况是所有文本都在同一个文本节点内,但这是一个更简单的情况。

您实际上需要做的是:

for each Element that may contain a <span>:
    for each child node in the element:
       generate the text content of this node and all following siblings
       match the target string/regex against the whole text
       if there is no match:
           break the outer loop - on to the next element.
       if the current node is an element node and the index of the match is not 0:
           break the inner loop - on to the next sibling node
       if the current node is a text node and the index of the match is > the length of the Text node data:
           break the inner loop - on to the next sibling node
       // now we have to find the position of the end of the match
       n is the length of the match string
       iterate through the remaining text node data and sibling text content:
           compare the length of the text content with n
           less?:
               subtract length from n and continue
           same?:
               we've got a match on a node boundary
               split the first text node if necessary
               insert a new span into the document
               move all the nodes from the first text node to this boundary inside the span
               break to outer loop, next element
           greater?:
               we've got a match ending inside the node.
               is the node a text node?:
                   then we can split the text node
                   also split the first text node if necessary
                   insert a new span into the document
                   move all contained nodes inside the span
                   break to outer loop, next element
               no, an element?:
                   oh dear! We can't insert a span here

哎呀。

这里有一个稍微不那么严厉的替代建议,如果将每个匹配的文本节点分别包装起来是可接受的。所以:

<p>Oh, my</p> name <div><div>is</div><div> Josh

将会为您留下以下输出:

<p>Oh, <span class="marked">my</span></p>
<span class="marked"> name </span>
<div><div><span class="marked">is</span></div></div>
<span class="marked"> Josh</span>

根据你如何设置匹配样式,这可能看起来还不错。它也可以解决部分匹配在元素内导致嵌套错误的问题。

补充:忘了伪代码吧,我已经基本上写完了代码,不妨完成它。下面是后一种方法的JavaScript版本:

markTextInElement(document.body, /My\s+name\s+is\s+Josh/gi);


function markTextInElement(element, regexp) {
    var nodes= [];
    collectTextNodes(nodes, element);
    var datas= nodes.map(function(node) { return node.data; });
    var text= datas.join('');

    // Get list of [startnodei, startindex, endnodei, endindex] matches
    //
    var matches= [], match;
    while (match= regexp.exec(text)) {
        var p0= getPositionInStrings(datas, match.index, false);
        var p1= getPositionInStrings(datas, match.index+match[0].length, true);
        matches.push([p0[0], p0[1], p1[0], p1[1]]);
    }

    // Get list of nodes for each match, splitted at the edges of the
    // text. Reverse-iterate to avoid the splitting changing nodes we
    // have yet to process.
    //
    for (var i= matches.length; i-->0;) {
        var ni0= matches[i][0], ix0= matches[i][1], ni1= matches[i][2], ix1= matches[i][3];
        var mnodes= nodes.slice(ni0, ni1+1);
        if (ix1<nodes[ni1].length)
            nodes[ni1].splitText(ix1);
        if (ix0>0)
            mnodes[0]= nodes[ni0].splitText(ix0);

        // Replace each text node in the sublist with a wrapped version
        //
        mnodes.forEach(function(node) {
            var span= document.createElement('span');
            span.className= 'marked';
            node.parentNode.replaceChild(span, node);
            span.appendChild(node);
        });
    }
}

function collectTextNodes(texts, element) {
    var textok= [
        'applet', 'col', 'colgroup', 'dl', 'iframe', 'map', 'object', 'ol',
        'optgroup', 'option', 'script', 'select', 'style', 'table',
        'tbody', 'textarea', 'tfoot', 'thead', 'tr', 'ul'
    ].indexOf(element.tagName.toLowerCase()===-1)
    for (var i= 0; i<element.childNodes.length; i++) {
        var child= element.childNodes[i];
        if (child.nodeType===3 && textok)
            texts.push(child);
        if (child.nodeType===1)
            collectTextNodes(texts, child);
    };
}

function getPositionInStrings(strs, index, toend) {
    var ix= 0;
    for (var i= 0; i<strs.length; i++) {
        var n= index-ix, l= strs[i].length;
        if (toend? l>=n : l>n)
            return [i, n];
        ix+= l;
    }
    return [i, 0];
}


// We've used a few ECMAScript Fifth Edition Array features.
// Make them work in browsers that don't support them natively.
//
if (!('indexOf' in Array.prototype)) {
    Array.prototype.indexOf= function(find, i /*opt*/) {
        if (i===undefined) i= 0;
        if (i<0) i+= this.length;
        if (i<0) i= 0;
        for (var n= this.length; i<n; i++)
            if (i in this && this[i]===find)
                return i;
        return -1;
    };
}
if (!('forEach' in Array.prototype)) {
    Array.prototype.forEach= function(action, that /*opt*/) {
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this)
                action.call(that, this[i], i, this);
    };
}
if (!('map' in Array.prototype)) {
    Array.prototype.map= function(mapper, that /*opt*/) {
        var other= new Array(this.length);
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this)
                other[i]= mapper.call(that, this[i], i, this);
        return other;
    };
}

你不能使用正则表达式解析XHTML。https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454... 哦,不用在意 :p - kennytm
哦,糟糕,我又“维基”了自己。 - bobince

0

它会找到 $textbody = "这本书很<b>难</b>找到。" 吗? - Alon Gubkin
是的,它可以工作,但对于$word = "非常困难";无法正常运行。 - Blair McMillan

0

XSL是这种工作的正确工具。你可以像这样做:

<?php    
$oldXml= <<<EOT
<html>
<head>
    <title>My Name Is Josh!!!</title>
</head>
<body>
    <h1>my name is <b>josh</b></h1>
    <div>
        <a href="http://www.names.com">my name</a> is josh
    </div>

    <u>my</u> <i>name</i> <b>is</b> <span style="font-family: Tahoma;">Josh</span>.
</body>
</html>
EOT;

$temp = <<<EOT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     version="1.0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="*">
<xsl:copy><xsl:copy-of select="@*"/><xsl:apply-templates/></xsl:copy>
</xsl:template>
<xsl:template match="text()">
<span class="marked">
<xsl:value-of select="current()"/>
</span>
</xsl:template>
</xsl:stylesheet>
EOT;    

$xml = new DOMDocument;
$xml->loadXML($oldXml);
$xsl = new DOMDocument;
$xsl->loadXML($temp);
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl); // attach the xsl rules    
$newXml = $proc->transformToXML($xml);
echo $newXml;

HTML必须是格式良好的XHTML才能进行此项工作。


这不就是匹配每个文本节点吗? - bobince

0

如果你想将相同的类应用于每个元素,那么你需要深入了解正则表达式的黑暗森林,但我不确定这样做有什么价值。如果你非常想让每个元素都有一个新的span,那么这个页面可能会有所帮助:http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx

实际上,更合理的做法是将class="marked"应用于body元素,除非你有充分的理由为页面上的所有内容添加重复的类。


1
他并不是将它应用于页面上的所有内容,而只是他给定文本的实例;看起来他试图标记页面上给定搜索词的所有实例;显然,如果搜索词部分加粗或出现在链接中,它仍然应该被标记... - TJ Ellis

-1

这里我发布了你想要的内容。

$string='<html>
<head>
    <title>My Name Is Josh!!!</title>
</head>
<body>
    <h1>my name is <b>josh</b></h1>
    <div>
        <a href="http://www.names.com">my name</a> is josh
    </div>

    <u>my</u> <i>name</i> <b>is</b> <span style="font-family: Tahoma;">Josh</span>.
</body>
';
$string=preg_replace('/>.+</','><span class="marked">$0</span><',$string);
$string=str_replace('<<','<',$string);
$string=str_replace('>>','>',$string);
echo $string;

-1:使用preg处理(X)HTML很难证明其合理性,而且它也不能产生期望的输出,并且输出的(X)HTML格式不正确。 - Richard JP Le Guen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接