Как обернуть часть текста в node с помощью JavaScript
У меня есть сложная проблема. Я работаю над script, который принимает регулярное выражение в качестве ввода. Этот script затем находит все совпадения для этого регулярного выражения в документе и обертывает каждое соответствие в своем собственном <span> элемент. Жесткая часть заключается в том, что текст является форматированным html-документом, поэтому мой script должен перемещаться по DOM и применять регулярное выражение на нескольких текстовых узлах одновременно, выясняя, где он должен разделить текстовые узлы, если это необходимо.
Например, с регулярным выражением, которое фиксирует полные предложения, начинающиеся с заглавной буквы и заканчивающиеся на период, этот документ:
<p>
<b>HTML</b> is a language used to make <b>websites.</b>
It was developed by <i>CERN</i> employees in the early 90s.
<p>
Будет превращено в это:
<p>
<span><b>HTML</b> is a language used to make <b>websites.</b></span>
<span>It was developed by <i>CERN</i> employees in the early 90s.</span>
<p>
script затем возвращает список всех созданных интервалов.
У меня уже есть код, который находит все текстовые узлы и сохраняет их в списке вместе с их позицией по всему документу и их глубине. Вам действительно не нужно понимать, что код, который поможет мне и его рекурсивная структура, может быть немного запутанным. T Вначале я не уверен, как это сделать, это выяснить, какие элементы должны быть включены в диапазон.
function SmartNode(node, depth, start) {
this.node = node;
this.depth = depth;
this.start = start;
}
function findTextNodes(node, depth, start) {
var list = [];
var start = start || 0;
depth = (typeof depth !== "undefined" ? depth : -1);
if(node.nodeType === Node.TEXT_NODE) {
list.push(new SmartNode(node, depth, start));
} else {
for(var i=0; i < node.childNodes.length; ++i) {
list = list.concat(findTextNodes(node.childNodes[i], depth+1, start));
if(list.length) start += list[list.length-1].node.nodeValue.length;
}
}
return list;
}
Я полагаю, что сделаю строку из всего документа, запустим через него регулярное выражение и использую список, чтобы найти, какие узлы соответствуют совпадению регулярных выражений, а затем разбить текстовые узлы соответственно.
Но проблема возникает, когда у меня есть такой документ:
<p>
This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
</p>
Там предложение, которое начинается за пределами тега <a>
, но заканчивается внутри него. Теперь я не хочу, чтобы script разделил эту ссылку на два тега. В более сложном документе это может испортить страницу, если это произойдет. Код может либо обернуть два предложения вместе:
<p>
<span>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></span>
</p>
Или просто оберните каждую часть в свой собственный элемент:
<p>
<span>This program is </span>
<a href="beta.html">
<span>not stable yet.</span>
<span>Do not use this in production yet.</span>
</a>
</p>
Может быть параметр, чтобы указать, что он должен делать. Я просто не уверен, что как выяснить, когда произойдет невозможное сокращение, и как его восстановить.
Другая проблема возникает, когда у меня есть пробел внутри дочернего элемента вроде:
<p>This is a <b>sentence. </b></p>
Технически совпадение регулярных выражений заканчивается сразу после периода, до конца тега <b>
. Однако было бы гораздо лучше рассмотреть пространство как часть матча и обернуть его следующим образом:
<p><span>This is a <b>sentence. </b></span></p>
Чем это:
<p><span>This is a </span><b><span>sentence.</span> </b></p>
Но это второстепенная проблема. В конце концов, я мог бы просто добавить лишнее белое пространство в регулярное выражение.
Я знаю, что это может звучать как вопрос "сделай это для меня", и это не тот быстрый вопрос, который мы видим на SO на ежедневной основе, но я застрял на нем какое-то время, и это для открытого -source library, над которой я работаю. Решение этой проблемы является последним препятствием. Если вы считаете, что другой сайт SE лучше всего подходит для этого вопроса, перенаправите меня, пожалуйста.
Ответы
Ответ 1
Вот два способа справиться с этим.
Я не знаю, соответствует ли вам следующее. Это достаточно простое решение проблемы, но по крайней мере не использует RegEx для управления тегами HTML. Он выполняет сопоставление образцов с сырым текстом, а затем использует DOM для управления содержимым.
Первый подход
Этот подход создает только один тег <span>
для каждого совпадения, используя некоторые менее распространенные API-интерфейсы браузера.
(См. Основную проблему этого подхода ниже демонстрации, и если не уверен, используйте второй подход).
Класс Range
представляет фрагмент текста. Он имеет функцию surroundContents
, которая позволяет обернуть диапазон в элементе. Кроме того, он имеет оговорку:
Этот метод почти эквивалентен newNode.appendChild(range.extractContents()); range.insertNode(newNode)
. После окружения граничные точки диапазона включают newNode
.
Исключение будет выдано, однако, если Range
разбивает не Text
node только с одной из своих граничных точек. То есть, в отличие от вышеприведенной альтернативы, если есть частично выбранные узлы, они не будут клонированы, и вместо этого операция завершится неудачно.
Ну, обходной путь предоставляется в MDN, так что все хорошо.
Итак, вот алгоритм:
Здесь моя реализация с демо:
function highlight(element, regex) {
var document = element.ownerDocument;
var getNodes = function() {
var nodes = [],
offset = 0,
node,
nodeIterator = document.createNodeIterator(element, NodeFilter.SHOW_TEXT, null, false);
while (node = nodeIterator.nextNode()) {
nodes.push({
textNode: node,
start: offset,
length: node.nodeValue.length
});
offset += node.nodeValue.length
}
return nodes;
}
var nodes = getNodes(nodes);
if (!nodes.length)
return;
var text = "";
for (var i = 0; i < nodes.length; ++i)
text += nodes[i].textNode.nodeValue;
var match;
while (match = regex.exec(text)) {
// Prevent empty matches causing infinite loops
if (!match[0].length)
{
regex.lastIndex++;
continue;
}
// Find the start and end text node
var startNode = null, endNode = null;
for (i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (node.start + node.length <= match.index)
continue;
if (!startNode)
startNode = node;
if (node.start + node.length >= match.index + match[0].length)
{
endNode = node;
break;
}
}
var range = document.createRange();
range.setStart(startNode.textNode, match.index - startNode.start);
range.setEnd(endNode.textNode, match.index + match[0].length - endNode.start);
var spanNode = document.createElement("span");
spanNode.className = "highlight";
spanNode.appendChild(range.extractContents());
range.insertNode(spanNode);
nodes = getNodes();
}
}
// Test code
var testDiv = document.getElementById("test-cases");
var originalHtml = testDiv.innerHTML;
function test() {
testDiv.innerHTML = originalHtml;
try {
var regex = new RegExp(document.getElementById("regex").value, "g");
highlight(testDiv, regex);
}
catch(e) {
testDiv.innerText = e;
}
}
document.getElementById("runBtn").onclick = test;
test();
.highlight {
background-color: yellow;
border: 1px solid orange;
border-radius: 5px;
}
.section {
border: 1px solid gray;
padding: 10px;
margin: 10px;
}
<form class="section">
RegEx: <input id="regex" type="text" value="[A-Z].*?\." /> <button id="runBtn">Highlight</button>
</form>
<div id="test-cases" class="section">
<div>foo bar baz</div>
<p>
<b>HTML</b> is a language used to make <b>websites.</b>
It was developed by <i>CERN</i> employees in the early 90s.
<p>
<p>
This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
</p>
<div>foo bar baz</div>
</div>
Ответ 2
Как все уже сказали, это скорее академический вопрос, поскольку на самом деле это не так, как вы это делаете. Это, как говорится, казалось забавным, поэтому здесь один подход.
EDIT: Думаю, теперь я понял суть этого.
function myReplace(str) {
myRegexp = /((^<[^>*]>)+|([^<>\.]*|(<[^\/>]*>[^<>\.]+<\/[^>]*>)+)*[^<>\.]*\.\s*|<[^>]*>|[^\.<>]+\.*\s*)/g;
arr = str.match(myRegexp);
var out = "";
for (i in arr) {
var node = arr[i];
if (node.indexOf("<")===0) out += node;
else out += "<span>"+node+"</span>"; // Here is where you would run whichever
// regex you want to match by
}
document.write(out.replace(/</g, "<").replace(/>/g, ">")+"<br>");
console.log(out);
}
myReplace('<p>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></p>');
myReplace('<p>This is a <b>sentence. </b></p>');
myReplace('<p>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</p>');
myReplace('<p>This is a <b>a sentence</b>. Followed <i>by</i> another one.</p>');
myReplace('<p>This is a <b>an even</b> more <i>complex sentence. </i></p>');
/* Will output:
<p><span>This program is </span><a href="beta.html"><span>not stable yet. </span><span>Do not use this in production yet.</span></a></p>
<p><span>This is a </span><b><span>sentence. </span></b></p>
<p><span>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</span></p>
<p><span>This is a <b>a sentence</b>. </span><span>Followed <i>by</i> another one.</span></p>
<p><span>This is a </span><b><span>an even</span></b><span> more </span><i><span>complex sentence. </span></i></p>
*/
Ответ 3
Я бы использовал представление "flat DOM" для такой задачи.
В плоском DOM этот абзац
<p>abc <a href="beta.html">def. ghij.</p>
будет представлен двумя векторами:
chars: "abc def. ghij.",
props: ....aaaaaaaaaa,
Вы будете использовать нормальное регулярное выражение для chars
для обозначения областей пролета в векторе реквизита:
chars: "abc def. ghij."
props: ssssaaaaaaaaaa
ssss sssss
Я использую схематическое представление здесь, эта реальная структура представляет собой массив массивов:
props: [
[s],
[s],
[s],
[s],
[a,s],
[a,s],
...
]
дерево преобразования - DOM ↔ flat-DOM может использовать простые автоматы состояния.
В конце вы преобразуете плоский DOM в DOM дерева, который будет выглядеть следующим образом:
<p><s>abc </s><a href="beta.html"><s>def.</s> <s>ghij.</s></p>
На всякий случай: я использую этот подход в своих редакторах HTML WYSIWYG.
Ответ 4
function parseText( element ){
var stack = [ element ];
var group = false;
var re = /(?!\s|$).*?(\.|$)/;
while ( stack.length > 0 ){
var node = stack.shift();
if ( node.nodeType === Node.TEXT_NODE )
{
if ( node.textContent.trim() != "" )
{
var match;
while( node && (match = re.exec( node.textContent )) )
{
var start = group ? 0 : match.index;
var length = match[0].length + match.index - start;
if ( start > 0 )
{
node = node.splitText( start );
}
var wrapper = document.createElement( 'span' );
var next = null;
if ( match[1].length > 0 ){
if ( node.textContent.length > length )
next = node.splitText( length );
group = false;
wrapper.className = "sentence sentence-end";
}
else
{
wrapper.className = "sentence";
group = true;
}
var parent = node.parentNode;
var sibling = node.nextSibling;
wrapper.appendChild( node );
if ( sibling )
parent.insertBefore( wrapper, sibling );
else
parent.appendChild( wrapper );
node = next;
}
}
}
else if ( node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE )
{
stack.unshift.apply( stack, node.childNodes );
}
}
}
parseText( document.body );
.sentence {
text-decoration: underline wavy red;
}
.sentence-end {
border-right: 1px solid red;
}
<p>This is a sentence. This is another sentence.</p>
<p>This sentence has <strong>emphasis</strong> inside it.</p>
<p><span>This sentence spans</span><span> two elements.</span></p>