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());
+ }
+}