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

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


The following commit(s) were added to refs/heads/main by this push:
     new 41de2e149399 CAMEL-23733: camel-jbang - Add properties syntax 
highlighting in TUI (#24001)
41de2e149399 is described below

commit 41de2e149399f8ed459c32b05ee11209080dbcc5
Author: Ravi <[email protected]>
AuthorDate: Sun Jun 14 14:12:33 2026 +0530

    CAMEL-23733: camel-jbang - Add properties syntax highlighting in TUI 
(#24001)
    
    Signed-off-by: Ravi <[email protected]>
---
 dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml     |   5 +
 .../jbang/core/commands/tui/SyntaxHighlighter.java |  81 ++++++++++++++++
 .../core/commands/tui/SyntaxHighlighterTest.java   | 103 +++++++++++++++++++++
 3 files changed, 189 insertions(+)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index 67ef8ad11dc4..62014014c056 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -78,6 +78,11 @@
             <artifactId>tamboui-markdown</artifactId>
             <version>${tamboui-version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-test-junit6</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
index dbd3b68ccdd2..8859a18ba387 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
@@ -33,6 +33,7 @@ class SyntaxHighlighter {
         JAVA,
         YAML,
         XML,
+        PROPERTIES,
         PLAIN
     }
 
@@ -88,6 +89,12 @@ class SyntaxHighlighter {
     private static final Style XML_ATTR_VALUE_STYLE = 
Style.EMPTY.fg(Color.GREEN);
     private static final Style XML_ENTITY_STYLE = Style.EMPTY.fg(Color.RED);
 
+    // Properties styles
+    private static final Style PROPERTIES_COMMENT_STYLE = 
Style.EMPTY.fg(Color.LIGHT_BLUE);
+    private static final Style PROPERTIES_KEY_STYLE = 
Style.EMPTY.fg(Color.YELLOW);
+    private static final Style PROPERTIES_SEPARATOR_STYLE = 
Style.EMPTY.fg(Color.WHITE).bold();
+    private static final Style PROPERTIES_VALUE_STYLE = 
Style.EMPTY.fg(Color.BLUE);
+
     private SyntaxHighlighter() {
     }
 
@@ -113,6 +120,7 @@ class SyntaxHighlighter {
             case "java" -> Language.JAVA;
             case "yaml", "yml", "camel.yaml", "camel.yml" -> Language.YAML;
             case "xml", "camel.xml" -> Language.XML;
+            case "properties" -> Language.PROPERTIES;
             default -> Language.PLAIN;
         };
     }
@@ -126,6 +134,7 @@ class SyntaxHighlighter {
             case JAVA -> highlightJava(text);
             case YAML -> highlightYaml(text);
             case XML -> highlightXml(text);
+            case PROPERTIES -> highlightProperties(text);
             default -> Line.from(List.of(Span.raw(text)));
         };
     }
@@ -245,6 +254,78 @@ class SyntaxHighlighter {
         return buildLine(text, charStyles);
     }
 
+    private static Line highlightProperties(String text) {
+        int len = text.length();
+        Style[] charStyles = new Style[len];
+
+        // skip leading whitespace (left unstyled, like the indentation)
+        int start = 0;
+        while (start < len && Character.isWhitespace(text.charAt(start))) {
+            start++;
+        }
+
+        // blank line
+        if (start >= len) {
+            return buildLine(text, charStyles);
+        }
+
+        // comment line: starts with # or !
+        char first = text.charAt(start);
+        if (first == '#' || first == '!') {
+            for (int i = start; i < len; i++) {
+                charStyles[i] = PROPERTIES_COMMENT_STYLE;
+            }
+            return buildLine(text, charStyles);
+        }
+
+        // key ends at the first unescaped '=', ':' or whitespace (Properties 
separators)
+        int keyEnd = -1;
+        for (int i = start; i < len; i++) {
+            char c = text.charAt(i);
+            if (c == '\\') {
+                i++; // skip the escaped character (e.g. \= \: \ )
+                continue;
+            }
+            if (c == '=' || c == ':' || Character.isWhitespace(c)) {
+                keyEnd = i;
+                break;
+            }
+        }
+
+        // key with no separator and no value (e.g. a lone "enabled")
+        if (keyEnd < 0) {
+            for (int i = start; i < len; i++) {
+                charStyles[i] = PROPERTIES_KEY_STYLE;
+            }
+            return buildLine(text, charStyles);
+        }
+
+        // key
+        for (int i = start; i < keyEnd; i++) {
+            charStyles[i] = PROPERTIES_KEY_STYLE;
+        }
+
+        // an explicit '=' or ':' separator may follow optional whitespace
+        int i = keyEnd;
+        while (i < len && Character.isWhitespace(text.charAt(i))) {
+            i++;
+        }
+        if (i < len && (text.charAt(i) == '=' || text.charAt(i) == ':')) {
+            charStyles[i] = PROPERTIES_SEPARATOR_STYLE;
+            i++;
+        }
+
+        // value (leading whitespace skipped, left unstyled)
+        while (i < len && Character.isWhitespace(text.charAt(i))) {
+            i++;
+        }
+        for (; i < len; i++) {
+            charStyles[i] = PROPERTIES_VALUE_STYLE;
+        }
+
+        return buildLine(text, charStyles);
+    }
+
     private static void applyPattern(Style[] charStyles, String text, Pattern 
pattern, Style style) {
         Matcher m = pattern.matcher(text);
         while (m.find()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
new file mode 100644
index 000000000000..addc69069c17
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 dev.tamboui.style.Color;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SyntaxHighlighterTest {
+
+    @Test
+    void detectsPropertiesLanguage() {
+        assertEquals(SyntaxHighlighter.Language.PROPERTIES,
+                SyntaxHighlighter.detectLanguage("application.properties"));
+        // extension matching is case-insensitive
+        assertEquals(SyntaxHighlighter.Language.PROPERTIES,
+                SyntaxHighlighter.detectLanguage("Application.PROPERTIES"));
+        // trailing line-number suffix is stripped before detection
+        assertEquals(SyntaxHighlighter.Language.PROPERTIES,
+                SyntaxHighlighter.detectLanguage("application.properties:12"));
+    }
+
+    @Test
+    void colorsKeySeparatorAndValue() {
+        Line line = SyntaxHighlighter.highlightLine("camel.main.name=demo", 
SyntaxHighlighter.Language.PROPERTIES);
+
+        assertEquals(Color.YELLOW, fg(line, "camel.main.name"));
+        assertEquals(Color.WHITE, fg(line, "="));
+        assertEquals(Color.BLUE, fg(line, "demo"));
+        assertRoundTrip(line, "camel.main.name=demo");
+    }
+
+    @Test
+    void colorsColonSeparatorWithSpaces() {
+        Line line = SyntaxHighlighter.highlightLine("server.port : 8080", 
SyntaxHighlighter.Language.PROPERTIES);
+
+        assertEquals(Color.YELLOW, fg(line, "server.port"));
+        assertEquals(Color.WHITE, fg(line, ":"));
+        assertEquals(Color.BLUE, fg(line, "8080"));
+        assertRoundTrip(line, "server.port : 8080");
+    }
+
+    @Test
+    void colorsComments() {
+        for (String comment : new String[] { "# a hash comment", "! a bang 
comment" }) {
+            Line line = SyntaxHighlighter.highlightLine(comment, 
SyntaxHighlighter.Language.PROPERTIES);
+            assertEquals(Color.LIGHT_BLUE, fg(line, comment));
+            assertRoundTrip(line, comment);
+        }
+    }
+
+    @Test
+    void colorsKeyWithoutValue() {
+        Line line = SyntaxHighlighter.highlightLine("enabled", 
SyntaxHighlighter.Language.PROPERTIES);
+        assertEquals(Color.YELLOW, fg(line, "enabled"));
+        assertRoundTrip(line, "enabled");
+    }
+
+    @Test
+    void preservesLeadingIndentationUnstyled() {
+        Line line = SyntaxHighlighter.highlightLine("  camel.x=1", 
SyntaxHighlighter.Language.PROPERTIES);
+        // the indentation is emitted as a raw (unstyled) span
+        assertEquals(null, fg(line, "  "));
+        assertEquals(Color.YELLOW, fg(line, "camel.x"));
+        assertEquals(Color.BLUE, fg(line, "1"));
+        assertRoundTrip(line, "  camel.x=1");
+    }
+
+    /** Returns the foreground color of the first span whose content equals 
{@code content}, or null. */
+    private static Color fg(Line line, String content) {
+        for (Span span : line.spans()) {
+            if (span.content().equals(content)) {
+                return span.style().fg().orElse(null);
+            }
+        }
+        throw new AssertionError("No span with content '" + content + "' in " 
+ line.rawContent());
+    }
+
+    private static void assertRoundTrip(Line line, String expected) {
+        StringBuilder sb = new StringBuilder();
+        for (Span span : line.spans()) {
+            sb.append(span.content());
+        }
+        assertEquals(expected, sb.toString());
+    }
+}

Reply via email to