├── .gitignore ├── README.md ├── bintray.gradle ├── build.gradle ├── examples ├── Example1Simple.java ├── Example1Simple.png ├── Example2Simple.java └── Example2Simple.png ├── install.gradle ├── settings.gradle └── src ├── main └── java │ └── net │ └── andreinc │ └── ansiscape │ ├── AnsiClass.java │ ├── AnsiScape.java │ ├── AnsiScapeContext.java │ ├── AnsiScapeContextException.java │ ├── AnsiSequence.java │ └── parser │ ├── Parser.java │ ├── ParserUtils.java │ ├── ParsingException.java │ └── tokens │ ├── EscapeClassBegin.java │ ├── EscapeClassEnd.java │ ├── FreeText.java │ ├── Token.java │ └── Tokenizer.java └── test └── java └── net └── andreinc └── ansiscape └── ParserUtilsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | .gradle/ 27 | .idea/* 28 | build/ 29 | gradle/ 30 | gradlew 31 | gradlew.bat 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnsiScape 2 | 3 | **AnsiScape** is a simple Java library that allows the user to format the output of the applications using [ANSI Escape Codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 4 | 5 | Library is available in [jcenter()](https://bintray.com/nomemory/maven/ansiscape). 6 | 7 | **gradle:** 8 | 9 | ```groovy 10 | repositories { 11 | jcenter() 12 | } 13 | 14 | dependencies { 15 | compile 'net.andreinc.ansiscape:ansiscape:0.0.2' 16 | } 17 | ``` 18 | 19 | **maven:** 20 | 21 | ```xml 22 | 23 | 24 | jcenter 25 | https://jcenter.bintray.com/ 26 | 27 | 28 | 29 | net.andreinc.ansiscape 30 | ansiscape 31 | 0.0.2 32 | 33 | ``` 34 | 35 | ### Example (1) 36 | 37 | ```java 38 | AnsiScape asciiScape = new AnsiScape(); 39 | 40 | String formatted = asciiScape.format("{b Some Bold Text {u Also Underlined}}"); 41 | String formatted2 = asciiScape.format("Romanian Flag {redBg }{yellowBg }{blueBg }"); 42 | 43 | System.out.println(formatted); 44 | System.out.println(formatted2); 45 | ``` 46 | 47 | The output: 48 | 49 | ![Example1Simple.png](https://github.com/nomemory/asciiscape/blob/master/examples/Example1Simple.png) 50 | 51 | ### Example (2) 52 | 53 | In this example we are defining our own escape codes classes (in a way similar to CSS classes): 54 | 55 | ```java 56 | AnsiScapeContext context = new AnsiScapeContext(); 57 | 58 | // Create new escape classes that can be used as tags inside the text 59 | AnsiClass title = AnsiClass.withName("title").add(AnsiSequence.BOLD); 60 | AnsiClass url = AnsiClass.withName("url").add(AnsiSequence.UNDERLINE, AnsiSequence.BLUE); 61 | AnsiClass text = AnsiClass.withName("text").add(AnsiSequence.RED); 62 | 63 | context.add(title).add(url).add(text); 64 | 65 | AnsiScape ansiScape = new AnsiScape(context); 66 | 67 | String format = ansiScape.format("{title Bold title}\n" + 68 | "-{text Some url: {url www.google.com}};\n" + 69 | "-{text Some other url: {url {redBg www.redbackground.com}}}"); 70 | 71 | System.out.println(format); 72 | ``` 73 | The output: 74 | 75 | ![Example2Simple.png](https://github.com/nomemory/asciiscape/blob/master/examples/Example2Simple.png) 76 | 77 | ### Supported classes 78 | 79 | By default the following escape classes can be used: 80 | 81 | | Escape Class | Description | 82 | | ------------- | -------------| 83 | | `{b }` | Bold text | 84 | | `{bold }` | Bold text | 85 | | `{dim }` | Dim text | 86 | | `{u }` | Underlined text | 87 | | `{underline }` | Underlined text | 88 | | `{blink }` | Blink text | 89 | | `{reverse }` | Reverse text | 90 | | `{blank }` | Blank | 91 | | `{overstrike }` | Overstrike text | 92 | | `{reset }` | Resets everything inside tag | 93 | | `{black }` | Black foreground | 94 | | `{blackBg }` | Black background | 95 | | `{red }` | Red foreground | 96 | | `{redBg }` | Red foreground | 97 | | `{green }` | Green foreground | 98 | | `{greenBg }` | Green background | 99 | | `{yellow }` | Yellow foreground | 100 | | `{yellowBg }` | Yellow background | 101 | | `{blue }` | Blue foreground | 102 | | `{blueBg }` | Blue background | 103 | | `{magenta }` | Magenta foreground | 104 | | `{magentaBg }` | Magenta background | 105 | | `{cyan }` | Cyan foreground | 106 | | `{cyanBg }` | Cyan background | 107 | | `{white }` | White foreground | 108 | | `{whiteBg }` | White background | 109 | 110 | ### Notes 111 | 112 | - Library woks well with Linux and Mac terminals. It wasn't tested on Windows, but as far as I know it won't work on Windows XP or older `cmd.exe` terminals without additional hacks ; 113 | - In this stage the library is **experimental** - there are no unit tests for the moment; 114 | - Some terminals (like IntelliJ Output Console) do not implement all the ascii codes. 115 | 116 | -------------------------------------------------------------------------------- /bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | 3 | version = '0.0.2' 4 | 5 | task sourcesJar(type: Jar) { 6 | classifier = 'sources' 7 | from sourceSets.main.allSource 8 | } 9 | 10 | task javadocJar(type: Jar, dependsOn: javadoc) { 11 | classifier 'javadoc' 12 | from javadoc.destinationDir 13 | } 14 | 15 | artifacts { 16 | archives javadocJar 17 | archives sourcesJar 18 | } 19 | 20 | // Bintray 21 | Properties properties = new Properties() 22 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 23 | 24 | bintray { 25 | user = properties.getProperty("bintray.user") 26 | key = properties.getProperty("bintray.apikey") 27 | 28 | configurations = ['archives'] 29 | pkg { 30 | repo = 'maven' 31 | name = 'ansiscape' 32 | desc = 'Color your output using Ansi Escape codes' 33 | websiteUrl = 'https://github.com/nomemory/ansiscape' 34 | vcsUrl = 'https://github.com/nomemory/ansiscape' 35 | licenses = ["Apache-2.0"] 36 | publish = true 37 | publicDownloadNumbers = true 38 | } 39 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript() { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' 7 | } 8 | } 9 | 10 | plugins { 11 | id "com.jfrog.bintray" version "1.7.3" 12 | } 13 | 14 | group 'net.andreinc.ansiscape' 15 | version '0.0.2' 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'idea' 19 | 20 | sourceCompatibility = 1.8 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | dependencies { 27 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.6' 28 | compileOnly 'org.projectlombok:lombok:1.16.18' 29 | testCompile group: 'junit', name: 'junit', version: '4.12' 30 | } 31 | 32 | apply from: 'install.gradle' 33 | apply from: 'bintray.gradle' 34 | -------------------------------------------------------------------------------- /examples/Example1Simple.java: -------------------------------------------------------------------------------- 1 | import net.andreinc.ansiscape.AnsiScape; 2 | 3 | public class Example1Simple { 4 | public static void main(String[] args) { 5 | AnsiScape asciiScape = new AnsiScape(); 6 | 7 | String formatted = asciiScape.format("{b Some Bold Text {u Also Underlined}}"); 8 | String formatted2 = asciiScape.format("Romanian Flag {redBg }{yellowBg }{blueBg }"); 9 | 10 | System.out.println(formatted); 11 | System.out.println(formatted2); 12 | } 13 | } -------------------------------------------------------------------------------- /examples/Example1Simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomemory/ansiscape/dce7c75b2591fecdd012216b3ab2b1c3b7756116/examples/Example1Simple.png -------------------------------------------------------------------------------- /examples/Example2Simple.java: -------------------------------------------------------------------------------- 1 | import net.andreinc.ansiscape.AnsiClass; 2 | import net.andreinc.ansiscape.AnsiScape; 3 | import net.andreinc.ansiscape.AnsiScapeContext; 4 | import net.andreinc.ansiscape.AnsiSequence; 5 | 6 | public class Example2Simple { 7 | public static void main(String[] args) { 8 | AnsiScapeContext context = new AnsiScapeContext(); 9 | 10 | AnsiClass title = AnsiClass.withName("title").add(AnsiSequence.BOLD); 11 | AnsiClass url = AnsiClass.withName("url").add(AnsiSequence.UNDERLINE, AnsiSequence.BLUE); 12 | AnsiClass text = AnsiClass.withName("text").add(AnsiSequence.RED); 13 | 14 | context.add(title).add(url).add(text); 15 | 16 | AnsiScape ansiScape = new AnsiScape(context); 17 | 18 | String format = ansiScape.format("{title Bold title}\n" + 19 | "-{text Some url: {url www.google.com}};\n" + 20 | "-{text Some other url: {url {redBg www.redbackground.com}}}"); 21 | 22 | System.out.println(format); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/Example2Simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomemory/ansiscape/dce7c75b2591fecdd012216b3ab2b1c3b7756116/examples/Example2Simple.png -------------------------------------------------------------------------------- /install.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | 3 | group = 'net.andreinc.ansiscape' 4 | 5 | install { 6 | repositories.mavenInstaller { 7 | pom { 8 | project { 9 | packaging 'jar' 10 | groupId 'net.andreinc.ansiscape' 11 | artifactId 'ansiscape' 12 | 13 | name 'ansiscape' // YOUR LIBRARY NAME 14 | description 'Color your output using Ansi Escape codes' 15 | url 'https://github.com/nomemory/ansiscape' 16 | 17 | licenses { 18 | license { 19 | name 'The Apache Software License, Version 2.0' 20 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 21 | } 22 | } 23 | developers { 24 | developer { 25 | id 'nomemory' //YOUR ID 26 | name 'Andrei N. Ciobanu' //YOUR NAME 27 | email 'n@n.com' //YOUR EMAIL 28 | } 29 | } 30 | scm { 31 | connection 'https://github.com/nomemory/ansiscape' // YOUR GIT REPO 32 | developerConnection 'https://github.com/nomemory/ansiscape' // YOUR GIT REPO 33 | url 'https://github.com/nomemory/ansiscape' // YOUR SITE 34 | 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ansiscape' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/AnsiClass.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static java.util.stream.Collectors.joining; 9 | import static org.apache.commons.lang3.StringUtils.isAlphanumeric; 10 | import static org.apache.commons.lang3.StringUtils.isEmpty; 11 | 12 | public class AnsiClass { 13 | 14 | @Getter private final String name; 15 | private final List ansiSequences = new ArrayList<>(); 16 | 17 | private AnsiClass(String name) { 18 | this.name = name; 19 | } 20 | 21 | public static final AnsiClass withName(String name) { 22 | if (isEmpty(name) || !isAlphanumeric(name)) { 23 | String errFmt = 24 | String.format("Invalid class name: '%s'. The name should be alphanumeric and non-empty.", name); 25 | throw new IllegalArgumentException(errFmt); 26 | } 27 | return new AnsiClass(name); 28 | } 29 | 30 | public AnsiClass add(AnsiSequence... sequences) { 31 | for (AnsiSequence seq : sequences) { 32 | ansiSequences.add(seq); 33 | } 34 | return this; 35 | } 36 | 37 | public AnsiClass add(String escape) { 38 | if (isEmpty(escape)) { 39 | String errFmt = 40 | String.format("Invalid escape sequence: '%s'. This should be non-empty.", escape); 41 | throw new IllegalArgumentException(errFmt); 42 | } 43 | add(new AnsiSequence(escape)); 44 | return this; 45 | } 46 | 47 | public AnsiClass inherit(AnsiClass ansiClass) { 48 | this.ansiSequences.addAll(ansiClass.ansiSequences); 49 | return this; 50 | } 51 | 52 | public String getCharSequences() { 53 | return ansiSequences.stream() 54 | .map(AnsiSequence::getSequence) 55 | .collect(joining("")); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "EscapeClass{" + 61 | "name='" + name + '\'' + 62 | ", escapeSequences=" + ansiSequences + 63 | '}'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/AnsiScape.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import net.andreinc.ansiscape.parser.Parser; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class AnsiScape { 11 | 12 | public static final AnsiScape ansi = new AnsiScape(); 13 | 14 | @Getter private AnsiScapeContext context = new AnsiScapeContext(); 15 | 16 | public String format(String source, Object... args) { 17 | Parser parser = new Parser(context, source); 18 | return String.format(parser.parse(source), args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/AnsiScapeContext.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import static net.andreinc.ansiscape.AnsiScapeContextException.asciiClassAlreadyDefined; 9 | import static net.andreinc.ansiscape.AnsiSequence.*; 10 | 11 | public class AnsiScapeContext { 12 | 13 | private final Map registeredClasses = new HashMap<>(); 14 | 15 | public AnsiScapeContext() { 16 | initDefaultClasses(); 17 | } 18 | 19 | private void initDefaultClasses() { 20 | registeredClasses.put("b", AnsiClass.withName("b").add(BOLD)); 21 | registeredClasses.put("bold", AnsiClass.withName("b").add(BOLD)); 22 | registeredClasses.put("dim", AnsiClass.withName("dim").add(DIM)); 23 | registeredClasses.put("u", AnsiClass.withName("u").add(UNDERLINE)); 24 | registeredClasses.put("underline", AnsiClass.withName("underline").add(UNDERLINE)); 25 | registeredClasses.put("blink", AnsiClass.withName("blink").add(BLINK)); 26 | registeredClasses.put("reverse", AnsiClass.withName("reverse").add(REVERSE)); 27 | registeredClasses.put("blank", AnsiClass.withName("blank").add(BLANK)); 28 | registeredClasses.put("overstrike", AnsiClass.withName("overstrike").add(OVERSTRIKE)); 29 | registeredClasses.put("reset", AnsiClass.withName("reset").add(RESET)); 30 | registeredClasses.put("black", AnsiClass.withName("black").add(BLACK)); 31 | registeredClasses.put("blackBg", AnsiClass.withName("blackBg").add(BLACK_BG)); 32 | registeredClasses.put("red", AnsiClass.withName("red").add(RED)); 33 | registeredClasses.put("redBg", AnsiClass.withName("redBg").add(RED_BG)); 34 | registeredClasses.put("green", AnsiClass.withName("green").add(GREEN)); 35 | registeredClasses.put("greenBg", AnsiClass.withName("greenBg").add(GREEN_BG)); 36 | registeredClasses.put("yellow", AnsiClass.withName("yellow").add(YELLOW)); 37 | registeredClasses.put("yellowBg", AnsiClass.withName("yellowBg").add(YELLOW_BG)); 38 | registeredClasses.put("blue", AnsiClass.withName("blue").add(BLUE)); 39 | registeredClasses.put("blueBg", AnsiClass.withName("blueBg").add(BLUE_BG)); 40 | registeredClasses.put("magenta", AnsiClass.withName("magenta").add(MAGENTA)); 41 | registeredClasses.put("magentaBg", AnsiClass.withName("blueBg").add(MAGENTA_BG)); 42 | registeredClasses.put("cyan", AnsiClass.withName("cyan").add(CYAN)); 43 | registeredClasses.put("cyanBg", AnsiClass.withName("cyan").add(CYAN_BG)); 44 | registeredClasses.put("white", AnsiClass.withName("white").add(WHITE)); 45 | registeredClasses.put("whiteBg", AnsiClass.withName("whiteBg").add(WHITE_BG)); 46 | } 47 | 48 | public Set getClassNames() { 49 | return Collections.unmodifiableSet(registeredClasses.keySet()); 50 | } 51 | 52 | public AnsiScapeContext add(AnsiClass ansiClass) { 53 | if (registeredClasses.containsKey(ansiClass.getName())) { 54 | throw asciiClassAlreadyDefined(ansiClass.getName()); 55 | } 56 | registeredClasses.put(ansiClass.getName(), ansiClass); 57 | return this; 58 | } 59 | 60 | public AnsiClass get(String className) { 61 | return registeredClasses.get(className); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/AnsiScapeContextException.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | public class AnsiScapeContextException extends RuntimeException { 4 | 5 | public static final AnsiScapeContextException asciiClassAlreadyDefined(String className) { 6 | return new AnsiScapeContextException(String.format("The '%s' already exists in the context. Please choose another name.", className)); 7 | } 8 | 9 | public AnsiScapeContextException(String msg) { 10 | super(msg); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/AnsiSequence.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | import lombok.Getter; 4 | 5 | import static org.apache.commons.lang3.StringUtils.isEmpty; 6 | 7 | public class AnsiSequence { 8 | 9 | private static final String ESCAPE_FORMAT = ((char)27) + "[%sm"; 10 | 11 | public static final AnsiSequence RESET = new AnsiSequence("0"); 12 | public static final AnsiSequence BOLD = new AnsiSequence("1"); 13 | public static final AnsiSequence DIM = new AnsiSequence("2"); 14 | public static final AnsiSequence UNDERLINE = new AnsiSequence("4"); 15 | public static final AnsiSequence BLINK = new AnsiSequence("5"); 16 | public static final AnsiSequence REVERSE = new AnsiSequence("7"); 17 | public static final AnsiSequence BLANK = new AnsiSequence("8"); 18 | public static final AnsiSequence OVERSTRIKE = new AnsiSequence("9"); 19 | 20 | public static final AnsiSequence BLACK = new AnsiSequence("30"); 21 | public static final AnsiSequence BLACK_BG = new AnsiSequence("40"); 22 | public static final AnsiSequence RED = new AnsiSequence("31"); 23 | public static final AnsiSequence RED_BG = new AnsiSequence("41"); 24 | public static final AnsiSequence GREEN = new AnsiSequence("32"); 25 | public static final AnsiSequence GREEN_BG = new AnsiSequence("42"); 26 | public static final AnsiSequence YELLOW = new AnsiSequence("33"); 27 | public static final AnsiSequence YELLOW_BG = new AnsiSequence("43"); 28 | public static final AnsiSequence BLUE = new AnsiSequence("34"); 29 | public static final AnsiSequence BLUE_BG = new AnsiSequence("44"); 30 | public static final AnsiSequence MAGENTA = new AnsiSequence("35"); 31 | public static final AnsiSequence MAGENTA_BG = new AnsiSequence("45"); 32 | public static final AnsiSequence CYAN = new AnsiSequence("36"); 33 | public static final AnsiSequence CYAN_BG = new AnsiSequence("46"); 34 | public static final AnsiSequence WHITE = new AnsiSequence("37"); 35 | public static final AnsiSequence WHITE_BG = new AnsiSequence("47"); 36 | 37 | public AnsiSequence(String escape) { 38 | if (isEmpty(escape)) { 39 | String errFmt = 40 | String.format("Invalid escape sequence: '%s'. This should be non-empty.", escape); 41 | throw new IllegalArgumentException(errFmt); 42 | } 43 | this.sequence = String.format(ESCAPE_FORMAT, escape); 44 | } 45 | 46 | @Getter private String sequence; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/Parser.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser; 2 | 3 | import net.andreinc.ansiscape.AnsiClass; 4 | import net.andreinc.ansiscape.AnsiScape; 5 | import net.andreinc.ansiscape.AnsiScapeContext; 6 | import net.andreinc.ansiscape.AnsiSequence; 7 | import net.andreinc.ansiscape.parser.tokens.*; 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | import static net.andreinc.ansiscape.parser.ParsingException.invalidBracketSerquence; 15 | import static net.andreinc.ansiscape.parser.ParsingException.unknownEscapeClass; 16 | 17 | public class Parser { 18 | 19 | private Tokenizer tokenizer; 20 | 21 | private AnsiScapeContext ansiScapeContext; 22 | private Set classNames; 23 | private final String source; 24 | 25 | public Parser(AnsiScapeContext ansiScapeContext, String source) { 26 | this.ansiScapeContext = ansiScapeContext; 27 | this.classNames = ansiScapeContext.getClassNames(); 28 | this.source = source; 29 | this.tokenizer = new Tokenizer(source); 30 | } 31 | 32 | public String parse(String source) { 33 | if (StringUtils.isEmpty(source)) { 34 | throw ParsingException.invalidSource(); 35 | } 36 | StringBuilder buff = new StringBuilder(); 37 | List tokens = tokenizer.getTokens(); 38 | LinkedList ansiClasses = new LinkedList<>(); 39 | for(Token token : tokens) { 40 | if (token instanceof EscapeClassBegin) { 41 | // Verify if the escape class is registered in the context 42 | EscapeClassBegin begin = (EscapeClassBegin) token; 43 | String escapeClassName = begin.getEscapeClassName(); 44 | AnsiClass ansiClass = ansiScapeContext.get(escapeClassName); 45 | if (null == ansiClass) { 46 | throw unknownEscapeClass(escapeClassName); 47 | } 48 | buff.append(ansiClass.getCharSequences()); 49 | ansiClasses.add(ansiClass); 50 | } 51 | else if (token instanceof FreeText) { 52 | buff.append(((FreeText) token).getText()); 53 | } 54 | else if (token instanceof EscapeClassEnd) { 55 | if (ansiClasses.isEmpty()) { 56 | throw invalidBracketSerquence(token.getStartIndex()); 57 | } 58 | ansiClasses.removeLast(); 59 | buff.append(AnsiSequence.RESET.getSequence()); 60 | for(AnsiClass ansiClass : ansiClasses) { 61 | buff.append(ansiClass.getCharSequences()); 62 | } 63 | } 64 | } 65 | return buff.toString(); 66 | } 67 | 68 | public static void main(String[] args) { 69 | AnsiScape ansiScape = new AnsiScape(); 70 | 71 | String fmt = ansiScape.format("{Green Test}{blue Test}{cyan Test}", "s"); 72 | System.out.println(fmt); 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/ParserUtils.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser; 2 | 3 | public class ParserUtils { 4 | 5 | /* 6 | * Returns all the characters starting from "start" until the end or a space is encountered. 7 | */ 8 | public static String getUntilSpaceOrEnd(String source, int start) { 9 | StringBuilder buff = new StringBuilder(); 10 | for(int i = start; i < source.length() && source.charAt(i) != ' '; ++i) { 11 | buff.append(source.charAt(i)); 12 | } 13 | return buff.toString(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/ParsingException.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser; 2 | 3 | public class ParsingException extends RuntimeException { 4 | 5 | public static final ParsingException unknownEscapeClass(String className) { 6 | return new ParsingException(String.format("Unknown escape class: '%s'.", className)); 7 | } 8 | 9 | public static final ParsingException invalidBracketSerquence(int index) { 10 | return new ParsingException(String.format("Invalid bracket sequence detected at index: %d.", index)); 11 | } 12 | 13 | public static final ParsingException invalidSource() { 14 | return new ParsingException("Invalid source. String cannot be NULL or empty."); 15 | } 16 | 17 | public ParsingException(String msg) { 18 | super(msg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/tokens/EscapeClassBegin.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser.tokens; 2 | 3 | import lombok.Getter; 4 | 5 | public class EscapeClassBegin extends Token { 6 | 7 | @Getter private String escapeClassName; 8 | 9 | public EscapeClassBegin(int index, String escapeClassName) { 10 | super(index); 11 | this.escapeClassName = escapeClassName; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "ESCAPE_CLASS_BEGIN:" + escapeClassName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/tokens/EscapeClassEnd.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser.tokens; 2 | 3 | public class EscapeClassEnd extends Token { 4 | 5 | public EscapeClassEnd(int startIndex) { 6 | super(startIndex); 7 | } 8 | 9 | @Override 10 | public String toString() { 11 | return "ESCAPE_CLASS_END"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/tokens/FreeText.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser.tokens; 2 | 3 | import lombok.Getter; 4 | 5 | public class FreeText extends Token { 6 | 7 | @Getter private String text; 8 | 9 | public FreeText(int index, String text) { 10 | super(index); 11 | this.text = text; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "FreeText{" + 17 | "text='" + text + '\'' + 18 | '}'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/tokens/Token.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser.tokens; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * This class represents the token that is obtained when parsing the text that 8 | * is formatted. 9 | */ 10 | @AllArgsConstructor 11 | public abstract class Token { 12 | 13 | /** 14 | * The index where the token was first detected. 15 | */ 16 | @Getter private int startIndex; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/andreinc/ansiscape/parser/tokens/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape.parser.tokens; 2 | 3 | import net.andreinc.ansiscape.parser.ParserUtils; 4 | 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | /** 9 | * This class splits the source String that is going to be formatted into a List of tokens. 10 | */ 11 | public class Tokenizer { 12 | 13 | private static final char CHR_ESCAPE = '`'; 14 | private static final char CHR_ESCAPE_CLASS_BEGIN = '{'; 15 | private static final char CHR_ESCAPE_CLASS_END = '}'; 16 | 17 | public final String source; 18 | 19 | public Tokenizer(String source) { 20 | this.source = source; 21 | } 22 | 23 | public List getTokens() { 24 | List tokens = new LinkedList<>(); 25 | 26 | int i = 0; 27 | char now; 28 | 29 | StringBuilder freeText = new StringBuilder(); 30 | 31 | while(i < source.length()) { 32 | now = source.charAt(i); 33 | // Escape char detected - ignoring next character 34 | if (now == CHR_ESCAPE) { 35 | freeText.append(source.charAt(i+1)); 36 | i++; 37 | } 38 | else if (now == CHR_ESCAPE_CLASS_BEGIN) { 39 | // This is where we exit from potential FreeText so everything we collected 40 | // so far is going to be added in the final list of tokens 41 | appendExistingFreeText(tokens, freeText, i); 42 | // We obtain the ascii escape class name 43 | String escapeClassName = ParserUtils.getUntilSpaceOrEnd(source, i+1); 44 | tokens.add(new EscapeClassBegin(i, escapeClassName)); 45 | // We skip to the character where the class name ends. 46 | i+=escapeClassName.length()+1; 47 | } 48 | else if (now == CHR_ESCAPE_CLASS_END) { 49 | // This is where we exit from potential FreeText so everything we collected 50 | // so far is going to be added in the final list of tokens 51 | appendExistingFreeText(tokens, freeText, i); 52 | tokens.add(new EscapeClassEnd(i)); 53 | } 54 | else { 55 | freeText.append(now); 56 | } 57 | i++; 58 | } 59 | // If there was anything in the freetext buffer we add it to the list of tokens 60 | appendExistingFreeText(tokens, freeText, i); 61 | return tokens; 62 | } 63 | 64 | public void appendExistingFreeText(List tokens, StringBuilder freeTextBuff, int idx) { 65 | if (freeTextBuff.length()!=0) { 66 | tokens.add(new FreeText(idx, freeTextBuff.toString())); 67 | freeTextBuff.setLength(0); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/net/andreinc/ansiscape/ParserUtilsTest.java: -------------------------------------------------------------------------------- 1 | package net.andreinc.ansiscape; 2 | 3 | import net.andreinc.ansiscape.parser.ParserUtils; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | public class ParserUtilsTest { 8 | 9 | @Test 10 | public void getUntilSpaceOrEndTest1() { 11 | String test = ParserUtils.getUntilSpaceOrEnd("Ana are mere", 0); 12 | Assert.assertTrue(test.equals("Ana")); 13 | } 14 | } 15 | --------------------------------------------------------------------------------