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/[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"));
+ }
+}