AnsiFormatter.java
/*
* Copyright 2026 Emmanuel Bourg
*
* Licensed 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 net.jsign;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to process text containing custom pseudo-XML tags for terminal styling.
*
* <p>Supported tags include text formatting (e.g., {@code <b>}, {@code <i>}) and standard text
* colors (e.g., {@code <red>}, {@code <green>}, etc.).</p>
*
* <p>Tags are either translated into standard ANSI escape sequences for compatible terminals, or stripped
* out entirely (useful for raw text logs or unsupported terminals). Unknown tags are left unchanged.</p>
*
* @since 8.0
*/
class AnsiFormatter {
private static final Pattern TAG_PATTERN = Pattern.compile("<(/)?(b|i|u|red|green|yellow|blue|magenta|cyan|white|gray)>");
private static final Map<String, String> REPLACEMENTS = new HashMap<>();
static {
REPLACEMENTS.put("b", "\033[1m");
REPLACEMENTS.put("i", "\033[3m");
REPLACEMENTS.put("u", "\033[4m");
REPLACEMENTS.put("red", "\033[31m");
REPLACEMENTS.put("green", "\033[32m");
REPLACEMENTS.put("yellow", "\033[33m");
REPLACEMENTS.put("blue", "\033[34m");
REPLACEMENTS.put("magenta", "\033[35m");
REPLACEMENTS.put("cyan", "\033[36m");
REPLACEMENTS.put("white", "\033[37m");
REPLACEMENTS.put("gray", "\033[90m");
}
private static final String RESET = "\033[0m";
private final boolean ansiEnabled;
/**
* Creates a new ANSI formatter. ANSI support is automatically detected.
*/
public AnsiFormatter() {
this.ansiEnabled = isAnsiSupported();
}
/**
* Creates a new ANSI formatter with the specified ANSI support.
*
* @param ansiEnabled {@code true} to convert tags to ANSI escape sequences;
* {@code false} to clean and remove the markup tags entirely
*/
public AnsiFormatter(boolean ansiEnabled) {
this.ansiEnabled = ansiEnabled;
}
/**
* Processes the input text according to the terminal's ANSI capability.
*
* @param text the input string containing custom markup tags
* @return the processed string, or {@code null} if the input was null
*/
public String format(String text) {
if (text == null || !text.contains("<")) {
return text;
}
Matcher matcher = TAG_PATTERN.matcher(text);
StringBuffer buf = new StringBuffer();
while (matcher.find()) {
String replacement;
if (ansiEnabled) {
boolean closing = matcher.group(1) != null;
String tagName = matcher.group(2);
replacement = closing ? RESET : REPLACEMENTS.get(tagName);
} else {
replacement = "";
}
matcher.appendReplacement(buf, replacement);
}
matcher.appendTail(buf);
return buf.toString();
}
/**
* Checks if the current terminal supports ANSI escape sequences.
*/
public static boolean isAnsiSupported() {
if (System.console() == null) {
return false;
}
if (System.getProperty("os.name").toLowerCase().contains("win")) {
if (System.getenv("WT_SESSION") != null) {
return true;
}
Integer windowsVersion = getWindowsVersion();
if (windowsVersion == null || windowsVersion < 10) {
return false;
}
try {
// "cmd.exe /c" triggers a call to SetConsoleMode and enables ANSI codes on the old Windows terminal (Windows 10+ only)
new ProcessBuilder("cmd.exe", "/c").inheritIO().start().waitFor(150, TimeUnit.MILLISECONDS);
} catch (Exception e) {
return false;
}
}
return true;
}
/**
* Returns the version of Windows.
*/
private static Integer getWindowsVersion() {
String version = System.getProperty("os.version");
Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)");
Matcher matcher = pattern.matcher(version);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
}
return null;
}
}