blob: eebbce7bd085504d929f5f72feb598e56a81ff27 [file] [log] [blame]
/*
* Copyright (c) 2002-2018, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package jdk.internal.org.jline.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Attributed string builder
*
* @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
*/
public class AttributedStringBuilder extends AttributedCharSequence implements Appendable {
private char[] buffer;
private int[] style;
private int length;
private TabStops tabs = new TabStops(0);
private int lastLineLength = 0;
private AttributedStyle current = AttributedStyle.DEFAULT;
public static AttributedString append(CharSequence... strings) {
AttributedStringBuilder sb = new AttributedStringBuilder();
for (CharSequence s : strings) {
sb.append(s);
}
return sb.toAttributedString();
}
public AttributedStringBuilder() {
this(64);
}
public AttributedStringBuilder(int capacity) {
buffer = new char[capacity];
style = new int[capacity];
length = 0;
}
@Override
public int length() {
return length;
}
@Override
public char charAt(int index) {
return buffer[index];
}
@Override
public AttributedStyle styleAt(int index) {
return new AttributedStyle(style[index], style[index]);
}
@Override
int styleCodeAt(int index) {
return style[index];
}
@Override
protected char[] buffer() {
return buffer;
}
@Override
protected int offset() {
return 0;
}
@Override
public AttributedString subSequence(int start, int end) {
return new AttributedString(
Arrays.copyOfRange(buffer, start, end),
Arrays.copyOfRange(style, start, end),
0,
end - start);
}
@Override
public AttributedStringBuilder append(CharSequence csq) {
return append(new AttributedString(csq, current));
}
@Override
public AttributedStringBuilder append(CharSequence csq, int start, int end) {
return append(csq.subSequence(start, end));
}
@Override
public AttributedStringBuilder append(char c) {
return append(Character.toString(c));
}
public AttributedStringBuilder append(CharSequence csq, AttributedStyle style) {
return append(new AttributedString(csq, style));
}
public AttributedStringBuilder style(AttributedStyle style) {
current = style;
return this;
}
public AttributedStringBuilder style(Function<AttributedStyle,AttributedStyle> style) {
current = style.apply(current);
return this;
}
public AttributedStringBuilder styled(Function<AttributedStyle,AttributedStyle> style, CharSequence cs) {
return styled(style, sb -> sb.append(cs));
}
public AttributedStringBuilder styled(AttributedStyle style, CharSequence cs) {
return styled(s -> style, sb -> sb.append(cs));
}
public AttributedStringBuilder styled(Function<AttributedStyle,AttributedStyle> style, Consumer<AttributedStringBuilder> consumer) {
AttributedStyle prev = current;
current = style.apply(prev);
consumer.accept(this);
current = prev;
return this;
}
public AttributedStyle style() {
return current;
}
public AttributedStringBuilder append(AttributedString str) {
return append((AttributedCharSequence) str, 0, str.length());
}
public AttributedStringBuilder append(AttributedString str, int start, int end) {
return append((AttributedCharSequence) str, start, end);
}
public AttributedStringBuilder append(AttributedCharSequence str) {
return append(str, 0, str.length());
}
public AttributedStringBuilder append(AttributedCharSequence str, int start, int end) {
ensureCapacity(length + end - start);
for (int i = start; i < end; i++) {
char c = str.charAt(i);
int s = str.styleCodeAt(i) & ~current.getMask() | current.getStyle();
if (tabs.defined() && c == '\t') {
insertTab(new AttributedStyle(s, 0));
} else {
ensureCapacity(length + 1);
buffer[length] = c;
style[length] = s;
if (c == '\n') {
lastLineLength = 0;
} else {
lastLineLength++;
}
length++;
}
}
return this;
}
protected void ensureCapacity(int nl) {
if (nl > buffer.length) {
int s = Math.max(buffer.length, 1);
while (s <= nl) {
s *= 2;
}
buffer = Arrays.copyOf(buffer, s);
style = Arrays.copyOf(style, s);
}
}
public void appendAnsi(String ansi) {
ansiAppend(ansi);
}
public AttributedStringBuilder ansiAppend(String ansi) {
int ansiStart = 0;
int ansiState = 0;
ensureCapacity(length + ansi.length());
for (int i = 0; i < ansi.length(); i++) {
char c = ansi.charAt(i);
if (ansiState == 0 && c == 27) {
ansiState++;
} else if (ansiState == 1 && c == '[') {
ansiState++;
ansiStart = i + 1;
} else if (ansiState == 2) {
if (c == 'm') {
String[] params = ansi.substring(ansiStart, i).split(";");
int j = 0;
while (j < params.length) {
int ansiParam = params[j].isEmpty() ? 0 : Integer.parseInt(params[j]);
switch (ansiParam) {
case 0:
current = AttributedStyle.DEFAULT;
break;
case 1:
current = current.bold();
break;
case 2:
current = current.faint();
break;
case 3:
current = current.italic();
break;
case 4:
current = current.underline();
break;
case 5:
current = current.blink();
break;
case 7:
current = current.inverse();
break;
case 8:
current = current.conceal();
break;
case 9:
current = current.crossedOut();
break;
case 22:
current = current.boldOff().faintOff();
break;
case 23:
current = current.italicOff();
break;
case 24:
current = current.underlineOff();
break;
case 25:
current = current.blinkOff();
break;
case 27:
current = current.inverseOff();
break;
case 28:
current = current.concealOff();
break;
case 29:
current = current.crossedOutOff();
break;
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
current = current.foreground(ansiParam - 30);
break;
case 39:
current = current.foregroundOff();
break;
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
current = current.background(ansiParam - 40);
break;
case 49:
current = current.backgroundOff();
break;
case 38:
case 48:
if (j + 1 < params.length) {
int ansiParam2 = Integer.parseInt(params[++j]);
if (ansiParam2 == 2) {
if (j + 3 < params.length) {
int r = Integer.parseInt(params[++j]);
int g = Integer.parseInt(params[++j]);
int b = Integer.parseInt(params[++j]);
// convert to 256 colors
int col = 16 + (r >> 3) * 36 + (g >> 3) * 6 + (b >> 3);
if (ansiParam == 38) {
current = current.foreground(col);
} else {
current = current.background(col);
}
}
} else if (ansiParam2 == 5) {
if (j + 1 < params.length) {
int col = Integer.parseInt(params[++j]);
if (ansiParam == 38) {
current = current.foreground(col);
} else {
current = current.background(col);
}
}
}
}
break;
case 90:
case 91:
case 92:
case 93:
case 94:
case 95:
case 96:
case 97:
current = current.foreground(ansiParam - 90 + 8);
break;
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
current = current.background(ansiParam - 100 + 8);
break;
}
j++;
}
ansiState = 0;
} else if (!(c >= '0' && c <= '9' || c == ';')) {
// This is not a SGR code, so ignore
ansiState = 0;
}
} else if (c == '\t' && tabs.defined()) {
insertTab(current);
} else {
ensureCapacity(length + 1);
buffer[length] = c;
style[length] = this.current.getStyle();
if (c == '\n') {
lastLineLength = 0;
} else {
lastLineLength++;
}
length++;
}
}
return this;
}
protected void insertTab(AttributedStyle s) {
int nb = tabs.spaces(lastLineLength);
ensureCapacity(length + nb);
for (int i = 0; i < nb; i++) {
buffer[length] = ' ';
style[length] = s.getStyle();
length++;
}
lastLineLength += nb;
}
public void setLength(int l) {
length = l;
}
/**
* Set the number of spaces a tab is expanded to. Tab size cannot be changed
* after text has been added to prevent inconsistent indentation.
*
* If tab size is set to 0, tabs are not expanded (the default).
* @param tabsize Spaces per tab or 0 for no tab expansion. Must be non-negative
* @return this
*/
public AttributedStringBuilder tabs(int tabsize) {
if (tabsize < 0) {
throw new IllegalArgumentException("Tab size must be non negative");
}
return tabs(Arrays.asList(tabsize));
}
public AttributedStringBuilder tabs(List<Integer> tabs) {
if (length > 0) {
throw new IllegalStateException("Cannot change tab size after appending text");
}
this.tabs = new TabStops(tabs);
return this;
}
public AttributedStringBuilder styleMatches(Pattern pattern, AttributedStyle s) {
Matcher matcher = pattern.matcher(this);
while (matcher.find()) {
for (int i = matcher.start(); i < matcher.end(); i++) {
style[i] = (style[i] & ~s.getMask()) | s.getStyle();
}
}
return this;
}
public AttributedStringBuilder styleMatches(Pattern pattern, List<AttributedStyle> styles) {
Matcher matcher = pattern.matcher(this);
while (matcher.find()) {
for (int group = 0; group < matcher.groupCount(); group++) {
AttributedStyle s = styles.get(group);
for (int i = matcher.start(group + 1); i < matcher.end(group + 1); i++) {
style[i] = (style[i] & ~s.getMask()) | s.getStyle();
}
}
}
return this;
}
private class TabStops {
private List<Integer> tabs = new ArrayList<>();
private int lastStop = 0;
private int lastSize = 0;
public TabStops(int tabs) {
this.lastSize = tabs;
}
public TabStops(List<Integer> tabs) {
this.tabs = tabs;
int p = 0;
for (int s: tabs) {
if (s <= p) {
continue;
}
lastStop = s;
lastSize = s - p;
p = s;
}
}
boolean defined() {
return lastSize > 0;
}
int spaces(int lastLineLength) {
int out = 0;
if (lastLineLength >= lastStop) {
out = lastSize - (lastLineLength - lastStop) % lastSize;
} else {
for (int s: tabs) {
if (s > lastLineLength) {
out = s - lastLineLength;
break;
}
}
}
return out;
}
}
}