Skip to content

Commit 493f258

Browse files
committed
Update 1.2.0
1 parent 86f578b commit 493f258

File tree

11 files changed

+279
-25
lines changed

11 files changed

+279
-25
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ for more info on ANSI escape codes.
2424
40–47 -> Set background color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))
2525
48 -> Set background color (Partial, see: [True-color](#supported-true-color-formats))
2626
49 -> Reset background color
27-
58 -> Set underline color (Only Android10+ & Partial, see: [True-color](#supported-true-color-formats))
28-
59 -> Reset underline color (Only Android10+)
27+
58\* -> Set underline color (Partial, see: [True-color](#supported-true-color-formats))
28+
59\* -> Reset underline color
2929
73 -> Superscript
3030
74 -> Subscript
3131
75 -> Neither superscript nor subscript
3232
90–97 -> Set bright foreground color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))
3333
100–107 -> Set bright background color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))
3434

35+
\*: Only Android10+ and unavailable with material compose compatibility layer.
36+
3537
## Supported True color formats
3638

3739
True color is `38`, `48`, or `58` with T + args.
@@ -70,9 +72,9 @@ repositories {
7072
7173
7274
dependencies {
73-
implementation 'com.github.Fox2Code.AndroidANSI:library:1.1.0'
75+
implementation 'com.github.Fox2Code.AndroidANSI:library:1.2.0'
7476
// You can also add the ktx module for the kotlin extension.
75-
implementation 'com.github.Fox2Code.AndroidANSI:library-ktx:1.1.0'
77+
implementation 'com.github.Fox2Code.AndroidANSI:library-ktx:1.2.0'
7678
}
7779
```
7880

androidansiapp/src/main/java/com/fox2code/androidansiapp/MainActivity.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import android.widget.TextView;
77

88
import com.fox2code.androidansi.AnsiParser;
9-
import com.fox2code.androidansi.AnsiTextView;
109

1110
public class MainActivity extends AppCompatActivity {
1211

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ android.disableAutomaticComponentCreation=true
2424
# Android targt SDK
2525
andorid.targetSdk=33
2626
# Library version
27-
library.version=1.1.0
27+
library.version=1.2.0

library-ktx/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ android {
4646
dependencies {
4747
api(project(":library"))
4848
compileOnly('androidx.appcompat:appcompat:1.6.1')
49+
compileOnly('androidx.compose.ui:ui-text:1.4.3')
4950
}
5051

5152
afterEvaluate {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.fox2code.androidansi.builder
2+
3+
import androidx.compose.ui.text.AnnotatedString
4+
import androidx.compose.ui.text.AnnotatedString.Builder
5+
import com.fox2code.androidansi.AnsiContext
6+
import com.fox2code.androidansi.ktx.toAnsiSpanStyle
7+
8+
class AnnotatedStringAnsiComponentBuilder : AnsiComponentBuilder<AnnotatedString>() {
9+
private val builder: Builder = Builder()
10+
private var used = false
11+
override fun notifyUse() {
12+
check(!used) { "AnnotatedStringAnsiComponentBuilder can only be used once!" }
13+
used = true
14+
}
15+
16+
override fun appendWithSpan(buffer: String, bufferStart: Int, bufferEnd: Int, ansiContext: AnsiContext, visibleStart: Int, visibleEnd: Int) {
17+
builder.append(buffer, bufferStart, bufferEnd)
18+
builder.addStyle(ansiContext.toAnsiSpanStyle(), visibleStart, visibleEnd)
19+
}
20+
21+
override fun build(): AnnotatedString {
22+
return builder.toAnnotatedString()
23+
}
24+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
2+
3+
package com.fox2code.androidansi.ktx
4+
5+
import androidx.compose.ui.graphics.Color
6+
import androidx.compose.ui.text.AnnotatedString
7+
import androidx.compose.ui.text.SpanStyle
8+
import androidx.compose.ui.text.font.FontStyle
9+
import androidx.compose.ui.text.font.FontWeight
10+
import androidx.compose.ui.text.style.BaselineShift
11+
import androidx.compose.ui.text.style.TextDecoration
12+
import com.fox2code.androidansi.AnsiConstants
13+
import com.fox2code.androidansi.AnsiContext
14+
import com.fox2code.androidansi.AnsiParser
15+
import com.fox2code.androidansi.builder.AnnotatedStringAnsiComponentBuilder
16+
import org.jetbrains.annotations.Contract
17+
18+
@Contract(pure = true)
19+
fun AnsiContext.toAnsiSpanStyle(): SpanStyle {
20+
var baselineShift: BaselineShift = BaselineShift.None
21+
if (this.style and AnsiConstants.FLAG_STYLE_SUBSCRIPT != 0) {
22+
baselineShift = BaselineShift.Subscript
23+
} else if (this.style and AnsiConstants.FLAG_STYLE_SUPERSCRIPT != 0) {
24+
baselineShift = BaselineShift.Superscript
25+
}
26+
val backgroundColor: Color = when(val background: ULong =
27+
this.background.toULong() or 0xFF000000.toULong()) {
28+
this.defaultBackground.toULong() -> Color.Unspecified
29+
0xFF000000.toULong() -> Color.Black
30+
0xFF444444.toULong() -> Color.DarkGray
31+
0xFF888888.toULong() -> Color.Gray
32+
0xFFCCCCCC.toULong() -> Color.LightGray
33+
0xFFFFFFFF.toULong() -> Color.White
34+
0xFFFF0000.toULong() -> Color.Red
35+
0xFF00FF00.toULong() -> Color.Green
36+
0xFF0000FF.toULong() -> Color.Blue
37+
0xFFFFFF00.toULong() -> Color.Yellow
38+
0xFFFFFF00.toULong() -> Color.Cyan
39+
0xFFFFFF00.toULong() -> Color.Magenta
40+
else -> Color(background)
41+
}
42+
var foregroundColor: Color = when(val foreground: ULong =
43+
this.foreground.toULong() or 0xFF000000.toULong()) {
44+
this.defaultForeground.toULong() -> Color.Unspecified
45+
0xFF000000.toULong() -> Color.Black
46+
0xFF444444.toULong() -> Color.DarkGray
47+
0xFF888888.toULong() -> Color.Gray
48+
0xFFCCCCCC.toULong() -> Color.LightGray
49+
0xFFFFFFFF.toULong() -> Color.White
50+
0xFFFF0000.toULong() -> Color.Red
51+
0xFF00FF00.toULong() -> Color.Green
52+
0xFF0000FF.toULong() -> Color.Blue
53+
0xFFFFFF00.toULong() -> Color.Yellow
54+
0xFFFFFF00.toULong() -> Color.Cyan
55+
0xFFFFFF00.toULong() -> Color.Magenta
56+
else -> Color(foreground)
57+
}
58+
val textDecoration: TextDecoration = when(this.style and (
59+
AnsiConstants.FLAG_STYLE_UNDERLINE or AnsiConstants.FLAG_STYLE_STRIKE
60+
)) {
61+
AnsiConstants.FLAG_STYLE_UNDERLINE or
62+
AnsiConstants.FLAG_STYLE_STRIKE ->
63+
TextDecoration.Underline.plus(TextDecoration.LineThrough)
64+
AnsiConstants.FLAG_STYLE_UNDERLINE -> TextDecoration.Underline
65+
AnsiConstants.FLAG_STYLE_STRIKE -> TextDecoration.LineThrough
66+
else -> TextDecoration.None
67+
}
68+
if (this.style and AnsiConstants.FLAG_STYLE_DIM != 0) {
69+
foregroundColor = foregroundColor.copy(alpha = (9F/16F))
70+
}
71+
val fontStyle: FontStyle? = when(this.style and AnsiConstants.FLAG_STYLE_ITALIC) {
72+
AnsiConstants.FLAG_STYLE_ITALIC -> FontStyle.Italic
73+
else -> null
74+
}
75+
val fontWeight: FontWeight? = when(this.style and AnsiConstants.FLAG_STYLE_BOLD) {
76+
AnsiConstants.FLAG_STYLE_BOLD -> FontWeight.Bold
77+
else -> null
78+
}
79+
return SpanStyle(baselineShift = baselineShift,
80+
textDecoration = textDecoration,
81+
background = backgroundColor,
82+
color = foregroundColor,
83+
fontStyle = fontStyle,
84+
fontWeight = fontWeight)
85+
}
86+
87+
@Contract(pure = true)
88+
inline fun AnsiContext.parseAsAnsiAnnotatedString(text: String, parseFlags: Int = 0): AnnotatedString {
89+
return AnsiParser.parseWithBuilder(AnnotatedStringAnsiComponentBuilder(), text, this, parseFlags)
90+
}
91+
92+
@Contract(pure = true)
93+
inline fun String.parseAsAnsiAnnotatedString(context: AnsiContext? = null, parseFlags: Int = 0): AnnotatedString {
94+
return AnsiParser.parseWithBuilder(AnnotatedStringAnsiComponentBuilder(), this, context, parseFlags)
95+
}

library-ktx/src/main/java/com/fox2code/androidansi/ktx/AnsiExtensions.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ const val FLAG_ANSI_PARSE_DISABLE_COLORS: Int = AnsiParser.FLAG_PARSE_DISABLE_CO
1818
const val FLAG_ANSI_PARSE_DISABLE_ATTRIBUTES: Int = AnsiParser.FLAG_PARSE_DISABLE_ATTRIBUTES
1919
const val FLAG_ANSI_PARSE_DISABLE_EXTRAS_COLORS: Int = AnsiParser.FLAG_PARSE_DISABLE_EXTRAS_COLORS
2020
const val FLAG_ANSI_PARSE_DISABLE_SUBSCRIPT: Int = AnsiParser.FLAG_PARSE_DISABLE_SUBSCRIPT
21+
const val FLAG_ANSI_PARSE_DISABLE_FONT_ALTERING: Int = AnsiParser.FLAG_PARSE_DISABLE_FONT_ALTERING
2122
const val FLAGS_ANSI_PARSE_DISABLE_ALL: Int = AnsiParser.FLAGS_DISABLE_ALL
2223

2324
@Contract(pure = true)
2425
inline fun String.patchAnsiEscapeSequences(): String = AnsiParser.patchEscapeSequences(this)
2526
@Contract(pure = true)
2627
inline fun String.removeAnsiEscapeSequences(): String = AnsiParser.removeEscapeSequences(this)
28+
@Contract(pure = true)
29+
inline fun String.removeAllAnsiDecorations(): String = AnsiParser.removeAllDecorations(this)
2730

28-
inline fun String.parseAsAnsi(context: AnsiContext? = null, parseFlags: Int = 0): Spannable {
31+
inline fun String.parseAsAnsiSpannable(context: AnsiContext? = null, parseFlags: Int = 0): Spannable {
2932
return if (context == null) {
3033
AnsiParser.parseAsSpannable(this, parseFlags)
3134
} else {

library/src/main/java/com/fox2code/androidansi/AnsiParser.java

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import android.graphics.Color;
44
import android.text.Spannable;
5-
import android.text.SpannableString;
6-
import android.text.SpannableStringBuilder;
75
import android.util.Log;
86
import android.widget.TextView;
97

108
import androidx.annotation.NonNull;
119
import androidx.annotation.Nullable;
10+
11+
import com.fox2code.androidansi.builder.AnsiComponentBuilder;
12+
import com.fox2code.androidansi.builder.SpannableAnsiComponentBuilder;
13+
import com.fox2code.androidansi.builder.StringAnsiComponentBuilder;
14+
1215
import org.jetbrains.annotations.Contract;
1316

1417
import java.util.Arrays;
@@ -18,18 +21,22 @@ public final class AnsiParser {
1821
// ANSI Has 2 escape sequences, let support both
1922
private static final String ESCAPE1 = "\\e[";
2023
private static final String ESCAPE2 = "\u001B[";
24+
private static final StringAnsiComponentBuilder RESETTABLE_BUILDER =
25+
new StringAnsiComponentBuilder(true);
2126

2227
// Any disabled attributes are unmodified.
2328
// Disable colors, implies FLAG_PARSE_DISABLE_EXTRAS_COLORS
2429
public static final int FLAG_PARSE_DISABLE_COLORS = 0x0001;
2530
// Disable attributes like italic, bold, underline and crossed out text
26-
// implies FLAG_PARSE_DISABLE_SUBSCRIPT
31+
// implies FLAG_PARSE_DISABLE_SUBSCRIPT and FLAG_PARSE_DISABLE_FONT_ALTERING
2732
public static final int FLAG_PARSE_DISABLE_ATTRIBUTES = 0x0002;
2833
// Disable extra color customization other than foreground or background
2934
// Useful to have consistent display across Android versions
3035
public static final int FLAG_PARSE_DISABLE_EXTRAS_COLORS = 0x0004;
3136
// Disable subscript and superscript text
3237
public static final int FLAG_PARSE_DISABLE_SUBSCRIPT = 0x0008;
38+
// Disable font altering component that may change text size
39+
public static final int FLAG_PARSE_DISABLE_FONT_ALTERING = 0x0010;
3340
// Disable all attributes changes
3441
public static final int FLAGS_DISABLE_ALL =
3542
FLAG_PARSE_DISABLE_COLORS | FLAG_PARSE_DISABLE_ATTRIBUTES;
@@ -52,6 +59,16 @@ public static String removeEscapeSequences(String string) {
5259
return string.replace(ESCAPE1, "").replace(ESCAPE2, "");
5360
}
5461

62+
/**
63+
* Remove all ANSI decoration for a given text
64+
*/
65+
@Contract(pure = true, value = "null -> fail")
66+
public static String removeAllDecorations(String string) {
67+
synchronized (RESETTABLE_BUILDER) {
68+
return parseWithBuilder(RESETTABLE_BUILDER, string, null, FLAGS_DISABLE_ALL);
69+
}
70+
}
71+
5572
@Contract(pure = true)
5673
public static Spannable parseAsSpannable(@NonNull String text) {
5774
return parseAsSpannable(text, null);
@@ -72,11 +89,18 @@ public static Spannable parseAsSpannable(
7289
@Contract(pure = true)
7390
public static Spannable parseAsSpannable(
7491
@NonNull String text, @Nullable AnsiContext ansiContext, int parseFlags) {
75-
if (text.length() == 0) return new SpannableString(text);
76-
ColorTransformer transformer = AnsiConstants.NO_OP_TRANSFORMER;
92+
return parseWithBuilder(new SpannableAnsiComponentBuilder(), text, ansiContext, parseFlags);
93+
}
94+
95+
public static <T extends CharSequence> T parseWithBuilder(
96+
@NonNull AnsiComponentBuilder<T> builder, @NonNull String text,
97+
@Nullable AnsiContext ansiContext, int parseFlags) {
98+
if (text.isEmpty()) {
99+
builder.notifyUse();
100+
return builder.build();
101+
}
77102
ansiContext = (ansiContext == null ? AnsiContext.DARK : ansiContext).asMutable();
78-
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
79-
int index = 0, indexEx = 0;
103+
int index = 0, indexEx = 0, simulatedLength = 0;
80104
while (true) {
81105
int index1 = text.indexOf(ESCAPE1, indexEx);
82106
int index2 = text.indexOf(ESCAPE2, indexEx);
@@ -97,10 +121,11 @@ public static Spannable parseAsSpannable(
97121
int currentEnd = index2 == -1 || (index1 != -1
98122
&& index1 < index2) ? index1 : index2;
99123
if (currentEnd != index) {
100-
int nStart = spannableStringBuilder.length(),
101-
nEnd = nStart + (currentEnd - index);
102-
spannableStringBuilder.append(text, index, currentEnd);
103-
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), nStart, nEnd, 0);
124+
int addedLen = (currentEnd - index);
125+
int nStart = simulatedLength,
126+
nEnd = nStart + addedLen;
127+
simulatedLength += addedLen;
128+
builder.appendWithSpan(text, index, currentEnd, ansiContext, nStart, nEnd);
104129
}
105130
index = indexEx = i + 1;
106131
try {
@@ -112,12 +137,11 @@ public static Spannable parseAsSpannable(
112137
}
113138
int currentEnd = text.length();
114139
if (currentEnd != index) {
115-
int nStart = spannableStringBuilder.length(),
116-
nEnd = nStart + (currentEnd - index);
117-
spannableStringBuilder.append(text, index, currentEnd);
118-
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), nStart, nEnd, 0);
140+
// int nStart = simulatedLength; // IDEA optimizations
141+
int nEnd = simulatedLength + (currentEnd - index);
142+
builder.appendWithSpan(text, index, currentEnd, ansiContext, simulatedLength, nEnd);
119143
}
120-
return spannableStringBuilder;
144+
return builder.build();
121145
}
122146

123147
public static void parseTokens(String[] tokens, AnsiContext ansiContext,int parseFlags) {
@@ -134,7 +158,9 @@ public static void parseTokens(String[] tokens, AnsiContext ansiContext,int pars
134158
break;
135159
case 1:
136160
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_DIM;
137-
ansiContext.style |= AnsiConstants.FLAG_STYLE_BOLD;
161+
if ((parseFlags & FLAG_PARSE_DISABLE_FONT_ALTERING) == 0) {
162+
ansiContext.style |= AnsiConstants.FLAG_STYLE_BOLD;
163+
}
138164
break;
139165
case 2:
140166
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
@@ -150,7 +176,9 @@ public static void parseTokens(String[] tokens, AnsiContext ansiContext,int pars
150176
ansiContext.style |= AnsiConstants.FLAG_STYLE_STRIKE;
151177
break;
152178
case 21:
153-
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
179+
if ((parseFlags & FLAG_PARSE_DISABLE_FONT_ALTERING) == 0) {
180+
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
181+
}
154182
break;
155183
case 22:
156184
ansiContext.style &= ~(AnsiConstants.FLAG_STYLE_BOLD |
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.fox2code.androidansi.builder;
2+
3+
import androidx.annotation.NonNull;
4+
5+
import com.fox2code.androidansi.AnsiContext;
6+
7+
/**
8+
* This class is called when ANSI parsable text need to be transformed to formatted text.
9+
*/
10+
public abstract class AnsiComponentBuilder<T extends CharSequence> {
11+
/**
12+
* Can be overridden if you don't want your builder to be used multiple times.
13+
*/
14+
public void notifyUse() {}
15+
16+
/**
17+
* @param buffer input raw ansi buffer
18+
* @param bufferStart start index of part of buffer to read
19+
* @param bufferEnd end index of part of buffer to read
20+
* @param ansiContext current ansi context call
21+
* {@link AnsiContext#toAnsiTextSpan()} to set a span.
22+
* @param visibleStart start of simulated visible index if all strings were appended correctly
23+
* @param visibleEnd end of simulated visible index if all strings were appended correctly
24+
*/
25+
public abstract void appendWithSpan(@NonNull String buffer, int bufferStart, int bufferEnd,
26+
@NonNull AnsiContext ansiContext, int visibleStart, int visibleEnd);
27+
28+
@NonNull
29+
public abstract T build();
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.fox2code.androidansi.builder;
2+
3+
import android.text.Spannable;
4+
import android.text.SpannableStringBuilder;
5+
6+
import androidx.annotation.NonNull;
7+
8+
import com.fox2code.androidansi.AnsiContext;
9+
10+
public final class SpannableAnsiComponentBuilder extends AnsiComponentBuilder<Spannable> {
11+
private final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
12+
private boolean used = false;
13+
14+
@Override
15+
public void notifyUse() {
16+
if (this.used) {
17+
throw new IllegalStateException("SpannableAnsiComponentBuilder can only be used once!");
18+
}
19+
this.used = true;
20+
}
21+
22+
@Override
23+
public void appendWithSpan(@NonNull String buffer, int bufferStart, int bufferEnd,
24+
@NonNull AnsiContext ansiContext, int visibleStart, int visibleEnd) {
25+
spannableStringBuilder.append(buffer, bufferStart, bufferEnd);
26+
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), visibleStart, visibleEnd, 0);
27+
}
28+
29+
@NonNull
30+
@Override
31+
public Spannable build() {
32+
return spannableStringBuilder;
33+
}
34+
}

0 commit comments

Comments
 (0)