这会有些棘手。虽然你可以通过简单的正则表达式来实现,忽略标签内的任何内容,但类似以下的天真方法并不可取:
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('');
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]]);
}
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);
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];
}
if (!('indexOf' in Array.prototype)) {
Array.prototype.indexOf= function(find, i ) {
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 ) {
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 ) {
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;
};
}
<b>
和<u>
这样的内联元素被认为是“文本的实例”吗? - Richard JP Le Guen