This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 87cedb50512c [backport camel-4.22.x] chore: camel-jbang - tidy link 
targets before drawing a frame (#27662)
87cedb50512c is described below

commit 87cedb50512c599f50aca3c550c8e967bc758d4e
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Oct 10 15:32:58 2026 +0200

    [backport camel-4.22.x] chore: camel-jbang - tidy link targets before 
drawing a frame (#27662)
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  1 +
 .../dsl/jbang/core/commands/tui/LinkTargets.java   | 84 ++++++++++++++++++++
 .../jbang/core/commands/tui/LinkTargetsTest.java   | 89 ++++++++++++++++++++++
 3 files changed, 174 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index dd78dbc2c718..2e32d7699dfb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -1491,6 +1491,7 @@ public class CamelMonitor extends CamelCommand {
         }
         renderFooter(frame, mainChunks.get(3));
         applyThemeBaseColors(frame.buffer(), area);
+        LinkTargets.tidy(frame.buffer());
 
         recordingManager.updateBuffer(frame.buffer());
         recordingManager.processPendingScreenshot();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargets.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargets.java
new file mode 100644
index 000000000000..bb62af8c96ec
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargets.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Optional;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Hyperlink;
+import dev.tamboui.style.Style;
+
+/**
+ * Tidies the link targets of a rendered frame. A link target (the URL and the 
id of a link in the text, such as a
+ * markdown link in an AI answer or a document) is plain text, so control 
characters in it are dropped before the frame
+ * is drawn.
+ */
+final class LinkTargets {
+
+    private LinkTargets() {
+    }
+
+    /**
+     * Drops the control characters from the URL and the id of every link in 
the buffer.
+     */
+    static void tidy(Buffer buffer) {
+        Rect area = buffer.area();
+        for (int y = area.top(); y < area.bottom(); y++) {
+            for (int x = area.left(); x < area.right(); x++) {
+                Cell cell = buffer.get(x, y);
+                Optional<Hyperlink> link = cell.style().hyperlink();
+                if (link.isPresent()) {
+                    Hyperlink tidied = tidy(link.get());
+                    if (tidied != link.get()) {
+                        buffer.set(x, y, cell.style(withLink(cell.style(), 
tidied)));
+                    }
+                }
+            }
+        }
+    }
+
+    /** The link without control characters in its URL and id; the same 
instance when it has none. */
+    static Hyperlink tidy(Hyperlink link) {
+        String url = withoutControls(link.url());
+        String id = link.id().map(LinkTargets::withoutControls).orElse(null);
+        if (url.equals(link.url()) && (id == null || 
id.equals(link.id().get()))) {
+            return link;
+        }
+        return id != null ? Hyperlink.of(url, id) : Hyperlink.of(url);
+    }
+
+    private static Style withLink(Style style, Hyperlink link) {
+        return link.id().isPresent() ? style.hyperlink(link.url(), 
link.id().get()) : style.hyperlink(link.url());
+    }
+
+    /** The text without C0 controls (U+0000 to U+001F), DEL and C1 controls 
(U+0080 to U+009F). */
+    static String withoutControls(String text) {
+        StringBuilder sb = null;
+        for (int i = 0; i < text.length(); i++) {
+            char c = text.charAt(i);
+            boolean control = c < 0x20 || (c >= 0x7F && c <= 0x9F);
+            if (control && sb == null) {
+                sb = new StringBuilder(text.length()).append(text, 0, i);
+            } else if (!control && sb != null) {
+                sb.append(c);
+            }
+        }
+        return sb != null ? sb.toString() : text;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargetsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargetsTest.java
new file mode 100644
index 000000000000..3ff6931dd6e0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LinkTargetsTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
+import dev.tamboui.style.Hyperlink;
+import dev.tamboui.style.Style;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LinkTargetsTest {
+
+    @Test
+    void dropsControlCharactersFromText() {
+        
assertThat(LinkTargets.withoutControls("https://example.com/a\u0007b\u001bc\u009bd\u007fe";))
+                .isEqualTo("https://example.com/abcde";);
+    }
+
+    @Test
+    void keepsPlainTextAsIs() {
+        String url = "https://example.com/path?q=1;x=2#frag-é-世";;
+        assertThat(LinkTargets.withoutControls(url)).isSameAs(url);
+    }
+
+    @Test
+    void tidiesTheUrlAndIdOfALink() {
+        Hyperlink tidied = 
LinkTargets.tidy(Hyperlink.of("https://example.com/\u0007x";, "id\u001b1"));
+
+        assertThat(tidied.url()).isEqualTo("https://example.com/x";);
+        assertThat(tidied.id()).contains("id1");
+    }
+
+    @Test
+    void keepsALinkWithoutControlCharacters() {
+        Hyperlink link = Hyperlink.of("https://camel.apache.org";, "doc");
+        assertThat(LinkTargets.tidy(link)).isSameAs(link);
+    }
+
+    @Test
+    void tidiesTheLinksOfABufferKeepingTheCells() {
+        Buffer buffer = Buffer.empty(new Rect(0, 0, 4, 1));
+        Style style = 
Style.EMPTY.bold().hyperlink("https://example.com/\u0007";);
+        buffer.set(1, 0, new Cell("x", style));
+
+        LinkTargets.tidy(buffer);
+
+        Cell cell = buffer.get(1, 0);
+        assertThat(cell.symbol()).isEqualTo("x");
+        
assertThat(cell.style().hyperlink()).map(Hyperlink::url).contains("https://example.com/";);
+        
assertThat(cell.style().addModifiers()).isEqualTo(style.addModifiers());
+    }
+
+    @Test
+    void tidiesALinkOfMarkdownWithCharacterReferences() {
+        // character references in a link destination become the characters 
themselves
+        String markdown = "See [the docs](<https://example.com/&#7;&#27;[2J>) 
for details.";
+        Buffer buffer = Buffer.empty(new Rect(0, 0, 40, 1));
+        MarkdownView.builder().source(markdown).build().render(buffer.area(), 
buffer);
+
+        LinkTargets.tidy(buffer);
+
+        List<String> urls = new ArrayList<>();
+        for (int x = 0; x < 40; x++) {
+            buffer.get(x, 0).style().hyperlink().ifPresent(link -> 
urls.add(link.url()));
+        }
+        assertThat(urls).isNotEmpty().allMatch(url -> 
url.equals("https://example.com/[2J";));
+    }
+}

Reply via email to