我有一些纯文本和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>
......它应该返回:
<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>
感谢。
答案 0 :(得分: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
这些元素不能用span包裹。如果你运气不好,浏览器修正“纠正”你的无效输出可能会使页面的一半“标记”,或者弄乱页面布局。
所以你必须在解析的DOM级别而不是字符串黑客上执行此操作。您可以使用PHP解析整个字符串,处理它并重新序列化,但是如果从可访问性的角度来看它是可接受的,那么在JavaScript的浏览器端可能会更容易实现它,其中内容已被解析为DOM节点。
这仍然很难。 This question处理文本将在同一文本节点内的位置,但这是一个更简单的情况。
你真正需要做的是:
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>
可能看起来不错,具体取决于您如何设置匹配样式。它还可以解决部分内部匹配的错误问题。
ETA:哦,对于伪代码,无论如何我现在或多或少地写了代码,不妨完成它。这是后一种方法的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;
};
}
答案 1 :(得分:0)
你需要深入到Regex的黑暗树林中,但是如果你想将相同的类应用于每个元素,我不确定这样做会有什么价值。如果您对具有新跨度的每个元素都非常苛刻,那么此页面可能有所帮助:http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
真正更合乎逻辑的做法是将class =“marked”应用于body元素,除非你有充分的理由在页面上的所有内容中添加重复的类。
答案 2 :(得分:0)
取自http://www.php.net/manual/en/function.preg-quote.php
$textbody = "This book is very difficult to find.";
$word = "very";
$textbody = preg_replace ("/" . preg_quote($word) . "/",
"<i>" . $word . "</i>",
$textbody);
答案 3 :(得分: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才能完成这项工作。
答案 4 :(得分:-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;