Как обернуть часть текста тегом <span>или любым другим HTML-тегом без экранирования новой HTML-структуры? - PullRequest
2 голосов
/ 22 июня 2019

Я сопоставляю определенную строку в тексте элемента и хочу обернуть соответствующий текст в интервал, чтобы иметь возможность выбрать его и применить изменения позже, но html-сущности экранируются. Есть ли способ обернуть строку тегами html, если ее экранировать?

Я пытался использовать метод unescapeEntities()​, но в этом случае он не работает. wrap() не сработало. для ссылки на эти методы проверьте https://jsoup.org/apidocs/org/jsoup/parser/Parser.html

Текущий код:

for (Element div : doc.select("div")) {
    for (String input : listOfStrings) {
        if (div.ownText().contains(input)) {
            div.text(div.ownText().replaceFirst(input, "<span class=\"select-me\">" + input + "</span>"));
        }
    }
}

Желаемый вывод

<div>some text <span class="select-me">matched string</span></div>

фактическая выработка

<div>some text &lt;span class=&quot;select-me&quot;&gt;matched string&lt;/span&gt;</div>

Ответы [ 2 ]

3 голосов
/ 22 июня 2019

Исходя из вашего вопроса и комментариев, похоже, что вы хотите изменить только прямые текстовые узлы выбранного элемента без изменения текстового узла потенциальных внутренних элементов выбранного текста, поэтому в случае

<div>a b <span>b c</span></div> 

, если мыхотим изменить b мы изменяем только один, непосредственно помещенный в <div>, но не один в <span>.

<div>a b <span>b c</span></div> 
       ^       ^----don't modify because it is in <span>, not *directly* in <div>
       |
     modify

Текст не рассматривается как ElementNode как <div> <span> и т. д.,но в DOM это представляется как TextNode, поэтому, если у нас есть структура, подобная <div> a <span>b</span> c </div>, тогда ее представление в DOM будет

Element: <div>
├ Text: " a "
├ Element: <span>
│ └ Text: "b"
└ Text: " c "

Если мы хотим обернуть часть некоторого текста в<span> (или любой другой тег) мы фактически разделяем один TextNode

├ Text: "foo bar baz"

на серии:

├ Text: "foo "
├ Element: <span>
│ └ Text: "bar"
└ Text: " baz"

Чтобы создать решение, использующее эту идею TextNode API предоставляет нам очень ограниченный набор инструментов, но среди доступных методов мы можем использовать

  • splitText(index), который изменяет оригинальный TextNode, оставляя в нем "левую" сторону разделения ивозвращает новый TextNode, который содержит rОставшаяся (правая) сторона разделения, как если TextNode node1 удерживает "foo bar" после TextNode node2 = node1.splitText(3); node1, будет удерживать "foo", в то время как node2 будет удерживать " bar" и будет помещена как непосредственный брат после node1
  • wrap(htmlElement) (унаследованный от Node суперкласса), который оборачивает TextNode в ElementNode, представляющий htmlElement, например node.wrap("<span class='myClass'>"), приведет к <span class='myClass>text from node</span>.

С помощью вышеупомянутых «инструментов» мы можем создать метод, подобный

static void wrapTextWithElement(TextNode textNode, String strToWrap, String wrapperHTML) {

    while (textNode.text().contains(strToWrap)) {
        // separates part before strToWrap
        // and returns node starting with text we want
        TextNode rightNodeFromSplit = textNode.splitText(textNode.text().indexOf(strToWrap));

        // if there is more text after searched string we need to
        // separate it and handle in next iteration
        if (rightNodeFromSplit.text().length() > strToWrap.length()) {
            textNode = rightNodeFromSplit.splitText(strToWrap.length());
            // after separating remining part rightNodeFromSplit holds
            // only part which we ware looking for so lets wrap it
            rightNodeFromSplit.wrap(wrapperHTML);
        } else { // here we know that node is holding only text to wrap
            rightNodeFromSplit.wrap(wrapperHTML);
            return;// since textNode didn't change but we already handled everything
        }
    }
}

, который мы можем использовать следующим образом:

Document doc = Jsoup.parse("<div>b a b <span>b c</span> d b</div> ");
System.out.println("BEFORE CHANGES:");
System.out.println(doc);

Element id1 = doc.select("div").first();
for (TextNode textNode : id1.textNodes()) {
    wrapTextWithElement(textNode, "b", "<span class='x'>");
}

System.out.println();
System.out.println("AFTER CHANGES");
System.out.println(doc);

Результат:

BEFORE CHANGES:
<html>
 <head></head>
 <body>
  <div>
   b a b 
   <span>b c</span> d b
  </div> 
 </body>
</html>

AFTER CHANGES
<html>
 <head></head>
 <body>
  <div>
   <span class="x">b</span> a 
   <span class="x">b</span> 
   <span>b c</span> d 
   <span class="x">b</span>
  </div> 
 </body>
</html>
1 голос
/ 22 июня 2019

Подробное объяснение в комментариях:

import java.util.ArrayList;
import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;

public class StackOverflow56717248 {

    public static void main(String[] args) {
        List<String> listOfStrings = new ArrayList<>();
        listOfStrings.add("INPUT");
        Document doc = Jsoup.parse(
                "<div id=\"1\">some text 1</div>" +
                "<div id=\"2\"> node before <b>xxx</b> this one contains INPUT text <b>xxx</b> node after</div>");
        System.out.println("BEFORE: ");
        System.out.println(doc);
        // iterating over all the divs
        for (Element div : doc.select("div")) {
            // and input texts
            for (String input : listOfStrings) {
                // to find the one with desired text
                if (div.ownText().contains(input)) {
                    // when found we have to be aware that this node may not be the only child
                    // so we have to iterate over children nodes
                    for (int i = 0; i < div.childNodeSize(); i++) {
                        Node child = div.childNode(i);
                        // taking into account only TextNodes
                        if (child instanceof TextNode && ((TextNode) child).text().contains(input)) {
                            TextNode textNode = ((TextNode) child);
                            // when found the one matching we can split text node
                            // into two nodes breaking it on position of desired text
                            // which will be inserted as a next sibling node
                            int indexOfInputText = textNode.text().indexOf(input);
                            textNode.splitText(indexOfInputText);
                            // getting the next node (the one newly created!)
                            TextNode nodeWithInput = (TextNode) textNode.nextSibling();
                            // we have to split it again in case there is more text after the input text
                            nodeWithInput.splitText(input.length());
                            // now this node contains only input text so we can wrap it with whatever you want
                            nodeWithInput.wrap("<span class=\"select-me\"></span>");
                            break;
                        }
                    }
                }
            }
        }
        System.out.println("--------");
        System.out.println("RESULT:");
        System.out.println(doc);
    }

}
...