How to modify substrings of a Text

I have the following Problem:
I want to detect all links in a text object (no problem so far)…
… and then make a clickable link on just that substring.

My fist attempt was to get the String from Texts.toPlain() and make a new Text object with the links.
The problem with this is, that all formatting on the previous Text gets lost!

Is there a way i can add the links without changing the Text formattion?

Try constructing a TextBuilder using the Text, new TextBuilder(text)

1 Like

And then? I am kind of helpless here :sweat_smile:

If I understand the problem correctly, you have a Text object, say:

Text message = Texts.of(TextColors.BLUE, "Some text ", TextColors.RESET, "https://spongepowered.org", TextColors.RED, " More Text");

(Which looks like: screenshot )
and you want to make the link clickable.

First, we need to break down the Text instance into a TextBuilder in order to work on it.
TextBuilder builder = message.builder()
Now, inspect the builder, if it’s a TextBuilder.Literal, then inspect the contents (see below).
Iterate over every child in the builder, breaking the child down into a builder and doing the same inspection.
Then replace the child with the new builder.build() and re-build the entire Text object.

To inspect the contents of TextBuilder.Literal, call builder.getContent()
Use a regular expression to look for URL patterns, split the string into any URL found and create a new Text object with that URL. Add the new text substring as a child in the builder. Make sure to clear the previous content of the builder.
Here’s a working example of what I described. (Note that the regex and substring splitting came from Forge, ForgeHooks.newChatWithLinks)

private void doClickifyExample() {
    Player player = getPlayerSomehow();
    Text message = Texts.of(TextColors.BLUE, "Some text ", TextColors.RESET, "https://spongepowered.org", TextColors.RED, " More Text");
    Text newMessage = clickifyLinks(message);
    player.sendMessage(newMessage);
}

static final Pattern URL_PATTERN = Pattern.compile(
        //         schema                          ipv4            OR           namespace                 port     path         ends
        //   |-----------------|        |-------------------------|  |----------------------------|    |---------| |--|   |---------------|
        "((?:[a-z0-9]{2,}:\\/\\/)?(?:(?:[0-9]{1,3}\\.){3}[0-9]{1,3}|(?:[-\\w_\\.]{1,}\\.[a-z]{2,}?))(?::[0-9]{1,5})?.*?(?=[!\"\u00A7 \n]|$))",
        Pattern.CASE_INSENSITIVE);

private Text clickifyLinks(Text text) {
    TextBuilder builder = text.builder();
    builder.removeAll();
    for (Text child : text.getChildren()) {
        builder.append(clickifyLinks(child));
    }
    if (builder instanceof TextBuilder.Literal) {
        String content = ((TextBuilder.Literal) builder).getContent();
        ((TextBuilder.Literal) builder).content("");
        // If this was a forge mod, there's a method for this, ForgeHooks.newChatWithLinks
        // In fact, much of this is copied from there
        Matcher m = URL_PATTERN.matcher(content);
        int lastIndex = 0;
        while (m.find()) {
            builder.append(Texts.of(content.substring(lastIndex, m.start())));
            lastIndex = m.end();
            String url = content.substring(m.start(), m.end());
            TextBuilder childBuilder = Texts.builder(url);
            try {
                childBuilder.onClick(new ClickAction.OpenUrl(new URL(url)));
            } catch (MalformedURLException ignored) {
                // No can do
            }
            builder.append(childBuilder.build());
        }
        builder.append(Texts.of(content.substring(lastIndex)));
    }
    return builder.build();
}
3 Likes

(builder instanceof TextBuilder.Literal)

This was the thing i was looking for … i did not know that there are different builder classes :see_no_evil:
Thank you very much! :slight_smile:

You might also want to check for components inside the TextBuilder. Not sure how to do that, though.

EDIT: Sorry for the bump.