Skip to content

Commit

Permalink
Update 1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Fox2Code committed May 3, 2023
1 parent 86f578b commit 493f258
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 25 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ for more info on ANSI escape codes.
40–47 -> Set background color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))
48 -> Set background color (Partial, see: [True-color](#supported-true-color-formats))
49 -> Reset background color
58 -> Set underline color (Only Android10+ & Partial, see: [True-color](#supported-true-color-formats))
59 -> Reset underline color (Only Android10+)
58\* -> Set underline color (Partial, see: [True-color](#supported-true-color-formats))
59\* -> Reset underline color
73 -> Superscript
74 -> Subscript
75 -> Neither superscript nor subscript
90–97 -> Set bright foreground color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))
100–107 -> Set bright background color (See: [Wikipedia ANSI Page](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors))

\*: Only Android10+ and unavailable with material compose compatibility layer.

## Supported True color formats

True color is `38`, `48`, or `58` with T + args.
Expand Down Expand Up @@ -70,9 +72,9 @@ repositories {
dependencies {
implementation 'com.github.Fox2Code.AndroidANSI:library:1.1.0'
implementation 'com.github.Fox2Code.AndroidANSI:library:1.2.0'
// You can also add the ktx module for the kotlin extension.
implementation 'com.github.Fox2Code.AndroidANSI:library-ktx:1.1.0'
implementation 'com.github.Fox2Code.AndroidANSI:library-ktx:1.2.0'
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import android.widget.TextView;

import com.fox2code.androidansi.AnsiParser;
import com.fox2code.androidansi.AnsiTextView;

public class MainActivity extends AppCompatActivity {

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ android.disableAutomaticComponentCreation=true
# Android targt SDK
andorid.targetSdk=33
# Library version
library.version=1.1.0
library.version=1.2.0
1 change: 1 addition & 0 deletions library-ktx/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ android {
dependencies {
api(project(":library"))
compileOnly('androidx.appcompat:appcompat:1.6.1')
compileOnly('androidx.compose.ui:ui-text:1.4.3')
}

afterEvaluate {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.fox2code.androidansi.builder

import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedString.Builder
import com.fox2code.androidansi.AnsiContext
import com.fox2code.androidansi.ktx.toAnsiSpanStyle

class AnnotatedStringAnsiComponentBuilder : AnsiComponentBuilder<AnnotatedString>() {
private val builder: Builder = Builder()
private var used = false
override fun notifyUse() {
check(!used) { "AnnotatedStringAnsiComponentBuilder can only be used once!" }
used = true
}

override fun appendWithSpan(buffer: String, bufferStart: Int, bufferEnd: Int, ansiContext: AnsiContext, visibleStart: Int, visibleEnd: Int) {
builder.append(buffer, bufferStart, bufferEnd)
builder.addStyle(ansiContext.toAnsiSpanStyle(), visibleStart, visibleEnd)
}

override fun build(): AnnotatedString {
return builder.toAnnotatedString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.

package com.fox2code.androidansi.ktx

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import com.fox2code.androidansi.AnsiConstants
import com.fox2code.androidansi.AnsiContext
import com.fox2code.androidansi.AnsiParser
import com.fox2code.androidansi.builder.AnnotatedStringAnsiComponentBuilder
import org.jetbrains.annotations.Contract

@Contract(pure = true)
fun AnsiContext.toAnsiSpanStyle(): SpanStyle {
var baselineShift: BaselineShift = BaselineShift.None
if (this.style and AnsiConstants.FLAG_STYLE_SUBSCRIPT != 0) {
baselineShift = BaselineShift.Subscript
} else if (this.style and AnsiConstants.FLAG_STYLE_SUPERSCRIPT != 0) {
baselineShift = BaselineShift.Superscript
}
val backgroundColor: Color = when(val background: ULong =
this.background.toULong() or 0xFF000000.toULong()) {
this.defaultBackground.toULong() -> Color.Unspecified
0xFF000000.toULong() -> Color.Black
0xFF444444.toULong() -> Color.DarkGray
0xFF888888.toULong() -> Color.Gray
0xFFCCCCCC.toULong() -> Color.LightGray
0xFFFFFFFF.toULong() -> Color.White
0xFFFF0000.toULong() -> Color.Red
0xFF00FF00.toULong() -> Color.Green
0xFF0000FF.toULong() -> Color.Blue
0xFFFFFF00.toULong() -> Color.Yellow
0xFFFFFF00.toULong() -> Color.Cyan
0xFFFFFF00.toULong() -> Color.Magenta
else -> Color(background)
}
var foregroundColor: Color = when(val foreground: ULong =
this.foreground.toULong() or 0xFF000000.toULong()) {
this.defaultForeground.toULong() -> Color.Unspecified
0xFF000000.toULong() -> Color.Black
0xFF444444.toULong() -> Color.DarkGray
0xFF888888.toULong() -> Color.Gray
0xFFCCCCCC.toULong() -> Color.LightGray
0xFFFFFFFF.toULong() -> Color.White
0xFFFF0000.toULong() -> Color.Red
0xFF00FF00.toULong() -> Color.Green
0xFF0000FF.toULong() -> Color.Blue
0xFFFFFF00.toULong() -> Color.Yellow
0xFFFFFF00.toULong() -> Color.Cyan
0xFFFFFF00.toULong() -> Color.Magenta
else -> Color(foreground)
}
val textDecoration: TextDecoration = when(this.style and (
AnsiConstants.FLAG_STYLE_UNDERLINE or AnsiConstants.FLAG_STYLE_STRIKE
)) {
AnsiConstants.FLAG_STYLE_UNDERLINE or
AnsiConstants.FLAG_STYLE_STRIKE ->
TextDecoration.Underline.plus(TextDecoration.LineThrough)
AnsiConstants.FLAG_STYLE_UNDERLINE -> TextDecoration.Underline
AnsiConstants.FLAG_STYLE_STRIKE -> TextDecoration.LineThrough
else -> TextDecoration.None
}
if (this.style and AnsiConstants.FLAG_STYLE_DIM != 0) {
foregroundColor = foregroundColor.copy(alpha = (9F/16F))
}
val fontStyle: FontStyle? = when(this.style and AnsiConstants.FLAG_STYLE_ITALIC) {
AnsiConstants.FLAG_STYLE_ITALIC -> FontStyle.Italic
else -> null
}
val fontWeight: FontWeight? = when(this.style and AnsiConstants.FLAG_STYLE_BOLD) {
AnsiConstants.FLAG_STYLE_BOLD -> FontWeight.Bold
else -> null
}
return SpanStyle(baselineShift = baselineShift,
textDecoration = textDecoration,
background = backgroundColor,
color = foregroundColor,
fontStyle = fontStyle,
fontWeight = fontWeight)
}

@Contract(pure = true)
inline fun AnsiContext.parseAsAnsiAnnotatedString(text: String, parseFlags: Int = 0): AnnotatedString {
return AnsiParser.parseWithBuilder(AnnotatedStringAnsiComponentBuilder(), text, this, parseFlags)
}

@Contract(pure = true)
inline fun String.parseAsAnsiAnnotatedString(context: AnsiContext? = null, parseFlags: Int = 0): AnnotatedString {
return AnsiParser.parseWithBuilder(AnnotatedStringAnsiComponentBuilder(), this, context, parseFlags)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ const val FLAG_ANSI_PARSE_DISABLE_COLORS: Int = AnsiParser.FLAG_PARSE_DISABLE_CO
const val FLAG_ANSI_PARSE_DISABLE_ATTRIBUTES: Int = AnsiParser.FLAG_PARSE_DISABLE_ATTRIBUTES
const val FLAG_ANSI_PARSE_DISABLE_EXTRAS_COLORS: Int = AnsiParser.FLAG_PARSE_DISABLE_EXTRAS_COLORS
const val FLAG_ANSI_PARSE_DISABLE_SUBSCRIPT: Int = AnsiParser.FLAG_PARSE_DISABLE_SUBSCRIPT
const val FLAG_ANSI_PARSE_DISABLE_FONT_ALTERING: Int = AnsiParser.FLAG_PARSE_DISABLE_FONT_ALTERING
const val FLAGS_ANSI_PARSE_DISABLE_ALL: Int = AnsiParser.FLAGS_DISABLE_ALL

@Contract(pure = true)
inline fun String.patchAnsiEscapeSequences(): String = AnsiParser.patchEscapeSequences(this)
@Contract(pure = true)
inline fun String.removeAnsiEscapeSequences(): String = AnsiParser.removeEscapeSequences(this)
@Contract(pure = true)
inline fun String.removeAllAnsiDecorations(): String = AnsiParser.removeAllDecorations(this)

inline fun String.parseAsAnsi(context: AnsiContext? = null, parseFlags: Int = 0): Spannable {
inline fun String.parseAsAnsiSpannable(context: AnsiContext? = null, parseFlags: Int = 0): Spannable {
return if (context == null) {
AnsiParser.parseAsSpannable(this, parseFlags)
} else {
Expand Down
64 changes: 46 additions & 18 deletions library/src/main/java/com/fox2code/androidansi/AnsiParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.fox2code.androidansi.builder.AnsiComponentBuilder;
import com.fox2code.androidansi.builder.SpannableAnsiComponentBuilder;
import com.fox2code.androidansi.builder.StringAnsiComponentBuilder;

import org.jetbrains.annotations.Contract;

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

// Any disabled attributes are unmodified.
// Disable colors, implies FLAG_PARSE_DISABLE_EXTRAS_COLORS
public static final int FLAG_PARSE_DISABLE_COLORS = 0x0001;
// Disable attributes like italic, bold, underline and crossed out text
// implies FLAG_PARSE_DISABLE_SUBSCRIPT
// implies FLAG_PARSE_DISABLE_SUBSCRIPT and FLAG_PARSE_DISABLE_FONT_ALTERING
public static final int FLAG_PARSE_DISABLE_ATTRIBUTES = 0x0002;
// Disable extra color customization other than foreground or background
// Useful to have consistent display across Android versions
public static final int FLAG_PARSE_DISABLE_EXTRAS_COLORS = 0x0004;
// Disable subscript and superscript text
public static final int FLAG_PARSE_DISABLE_SUBSCRIPT = 0x0008;
// Disable font altering component that may change text size
public static final int FLAG_PARSE_DISABLE_FONT_ALTERING = 0x0010;
// Disable all attributes changes
public static final int FLAGS_DISABLE_ALL =
FLAG_PARSE_DISABLE_COLORS | FLAG_PARSE_DISABLE_ATTRIBUTES;
Expand All @@ -52,6 +59,16 @@ public static String removeEscapeSequences(String string) {
return string.replace(ESCAPE1, "").replace(ESCAPE2, "");
}

/**
* Remove all ANSI decoration for a given text
*/
@Contract(pure = true, value = "null -> fail")
public static String removeAllDecorations(String string) {
synchronized (RESETTABLE_BUILDER) {
return parseWithBuilder(RESETTABLE_BUILDER, string, null, FLAGS_DISABLE_ALL);
}
}

@Contract(pure = true)
public static Spannable parseAsSpannable(@NonNull String text) {
return parseAsSpannable(text, null);
Expand All @@ -72,11 +89,18 @@ public static Spannable parseAsSpannable(
@Contract(pure = true)
public static Spannable parseAsSpannable(
@NonNull String text, @Nullable AnsiContext ansiContext, int parseFlags) {
if (text.length() == 0) return new SpannableString(text);
ColorTransformer transformer = AnsiConstants.NO_OP_TRANSFORMER;
return parseWithBuilder(new SpannableAnsiComponentBuilder(), text, ansiContext, parseFlags);
}

public static <T extends CharSequence> T parseWithBuilder(
@NonNull AnsiComponentBuilder<T> builder, @NonNull String text,
@Nullable AnsiContext ansiContext, int parseFlags) {
if (text.isEmpty()) {
builder.notifyUse();
return builder.build();
}
ansiContext = (ansiContext == null ? AnsiContext.DARK : ansiContext).asMutable();
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
int index = 0, indexEx = 0;
int index = 0, indexEx = 0, simulatedLength = 0;
while (true) {
int index1 = text.indexOf(ESCAPE1, indexEx);
int index2 = text.indexOf(ESCAPE2, indexEx);
Expand All @@ -97,10 +121,11 @@ public static Spannable parseAsSpannable(
int currentEnd = index2 == -1 || (index1 != -1
&& index1 < index2) ? index1 : index2;
if (currentEnd != index) {
int nStart = spannableStringBuilder.length(),
nEnd = nStart + (currentEnd - index);
spannableStringBuilder.append(text, index, currentEnd);
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), nStart, nEnd, 0);
int addedLen = (currentEnd - index);
int nStart = simulatedLength,
nEnd = nStart + addedLen;
simulatedLength += addedLen;
builder.appendWithSpan(text, index, currentEnd, ansiContext, nStart, nEnd);
}
index = indexEx = i + 1;
try {
Expand All @@ -112,12 +137,11 @@ public static Spannable parseAsSpannable(
}
int currentEnd = text.length();
if (currentEnd != index) {
int nStart = spannableStringBuilder.length(),
nEnd = nStart + (currentEnd - index);
spannableStringBuilder.append(text, index, currentEnd);
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), nStart, nEnd, 0);
// int nStart = simulatedLength; // IDEA optimizations
int nEnd = simulatedLength + (currentEnd - index);
builder.appendWithSpan(text, index, currentEnd, ansiContext, simulatedLength, nEnd);
}
return spannableStringBuilder;
return builder.build();
}

public static void parseTokens(String[] tokens, AnsiContext ansiContext,int parseFlags) {
Expand All @@ -134,7 +158,9 @@ public static void parseTokens(String[] tokens, AnsiContext ansiContext,int pars
break;
case 1:
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_DIM;
ansiContext.style |= AnsiConstants.FLAG_STYLE_BOLD;
if ((parseFlags & FLAG_PARSE_DISABLE_FONT_ALTERING) == 0) {
ansiContext.style |= AnsiConstants.FLAG_STYLE_BOLD;
}
break;
case 2:
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
Expand All @@ -150,7 +176,9 @@ public static void parseTokens(String[] tokens, AnsiContext ansiContext,int pars
ansiContext.style |= AnsiConstants.FLAG_STYLE_STRIKE;
break;
case 21:
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
if ((parseFlags & FLAG_PARSE_DISABLE_FONT_ALTERING) == 0) {
ansiContext.style &= ~AnsiConstants.FLAG_STYLE_BOLD;
}
break;
case 22:
ansiContext.style &= ~(AnsiConstants.FLAG_STYLE_BOLD |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.fox2code.androidansi.builder;

import androidx.annotation.NonNull;

import com.fox2code.androidansi.AnsiContext;

/**
* This class is called when ANSI parsable text need to be transformed to formatted text.
*/
public abstract class AnsiComponentBuilder<T extends CharSequence> {
/**
* Can be overridden if you don't want your builder to be used multiple times.
*/
public void notifyUse() {}

/**
* @param buffer input raw ansi buffer
* @param bufferStart start index of part of buffer to read
* @param bufferEnd end index of part of buffer to read
* @param ansiContext current ansi context call
* {@link AnsiContext#toAnsiTextSpan()} to set a span.
* @param visibleStart start of simulated visible index if all strings were appended correctly
* @param visibleEnd end of simulated visible index if all strings were appended correctly
*/
public abstract void appendWithSpan(@NonNull String buffer, int bufferStart, int bufferEnd,
@NonNull AnsiContext ansiContext, int visibleStart, int visibleEnd);

@NonNull
public abstract T build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.fox2code.androidansi.builder;

import android.text.Spannable;
import android.text.SpannableStringBuilder;

import androidx.annotation.NonNull;

import com.fox2code.androidansi.AnsiContext;

public final class SpannableAnsiComponentBuilder extends AnsiComponentBuilder<Spannable> {
private final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
private boolean used = false;

@Override
public void notifyUse() {
if (this.used) {
throw new IllegalStateException("SpannableAnsiComponentBuilder can only be used once!");
}
this.used = true;
}

@Override
public void appendWithSpan(@NonNull String buffer, int bufferStart, int bufferEnd,
@NonNull AnsiContext ansiContext, int visibleStart, int visibleEnd) {
spannableStringBuilder.append(buffer, bufferStart, bufferEnd);
spannableStringBuilder.setSpan(ansiContext.toAnsiTextSpan(), visibleStart, visibleEnd, 0);
}

@NonNull
@Override
public Spannable build() {
return spannableStringBuilder;
}
}
Loading

0 comments on commit 493f258

Please sign in to comment.