parquet.tools.util.PrettyPrintWriter.java Source code

Java tutorial

Introduction

Here is the source code for parquet.tools.util.PrettyPrintWriter.java

Source

/**
 * Copyright 2013 ARRIS, Inc.
 *
 * 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 parquet.tools.util;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;

import parquet.tools.Main;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;

public class PrettyPrintWriter extends PrintWriter {
    public static final String MORE = " [more]...";
    public static final String LINE_SEP = System.getProperty("line.separator");
    public static final Span DEFAULT_APPEND;
    public static final char DEFAULT_COLUMN_SEP = ':';
    public static final int DEFAULT_MAX_COLUMNS = 1;
    public static final int DEFAULT_COLUMN_PADDING = 1;
    public static final int DEFAULT_TABS = 4;
    public static final int DEFAULT_WIDTH;
    public static final int DEFAULT_COLORS;

    private static final String RESET = "\u001B[0m";

    public static final String MODE_OFF = "0";
    public static final String MODE_BOLD = "1";
    public static final String MODE_UNDER = "4";
    public static final String MODE_BLINK = "5";
    public static final String MODE_REVERSE = "7";
    public static final String MODE_CONCEALED = "8";

    public static final String FG_COLOR_BLACK = "30";
    public static final String FG_COLOR_RED = "31";
    public static final String FG_COLOR_GREEN = "32";
    public static final String FG_COLOR_YELLOW = "33";
    public static final String FG_COLOR_BLUE = "34";
    public static final String FG_COLOR_MAGENTA = "35";
    public static final String FG_COLOR_CYAN = "36";
    public static final String FG_COLOR_WHITE = "37";

    public static final String BG_COLOR_BLACK = "40";
    public static final String BG_COLOR_RED = "41";
    public static final String BG_COLOR_GREEN = "42";
    public static final String BG_COLOR_YELLOW = "43";
    public static final String BG_COLOR_BLUE = "44";
    public static final String BG_COLOR_MAGENTA = "45";
    public static final String BG_COLOR_CYAN = "46";
    public static final String BG_COLOR_WHITE = "47";

    public enum WhiteSpaceHandler {
        ELIMINATE_NEWLINES, COLLAPSE_WHITESPACE
    }

    static {
        int consoleWidth = 80;
        int numColors = 0;

        String columns = System.getenv("COLUMNS");
        if (columns != null && !columns.isEmpty()) {
            try {
                consoleWidth = Integer.parseInt(columns);
            } catch (Throwable th) {
            }
        }

        String colors = System.getenv("COLORS");
        if (colors != null && !colors.isEmpty()) {
            try {
                numColors = Integer.parseInt(colors);
                if (numColors < 0)
                    numColors = 0;
            } catch (Throwable th) {
            }
        }

        String termout = System.getenv("TERMOUT");
        if (termout != null && !termout.isEmpty()) {
            if (!"y".equalsIgnoreCase(termout) && !"yes".equalsIgnoreCase(termout) && !"t".equalsIgnoreCase(termout)
                    && !"true".equalsIgnoreCase(termout) && !"on".equalsIgnoreCase(termout)) {
                consoleWidth = Integer.MAX_VALUE;
                numColors = 0;
            }
        }

        if (System.getProperty("DISABLE_COLORS", null) != null) {
            numColors = 0;
        }

        DEFAULT_WIDTH = consoleWidth;
        DEFAULT_COLORS = numColors;

        if (numColors > 0) {
            DEFAULT_APPEND = mkspan(MORE, null, FG_COLOR_RED, null);
        } else {
            DEFAULT_APPEND = mkspan(MORE);
        }
    }

    private final StringBuilder formatString;
    private final Formatter formatter;
    private final ArrayList<Line> buffer;

    private final boolean autoColumn;
    private final boolean autoCrop;
    private final Span appendToLongLine;
    private final int consoleWidth;
    private final int tabWidth;
    private final char columnSeparator;
    private final int maxColumns;
    private final int columnPadding;
    private final long maxBufferedLines;
    private final boolean flushOnTab;
    private final WhiteSpaceHandler whiteSpaceHandler;
    private int tabLevel;
    private String colorMode;
    private String colorForeground;
    private String colorBackground;
    private String tabs;

    private PrettyPrintWriter(OutputStream out, boolean autoFlush, boolean autoColumn, boolean autoCrop,
            Span appendToLongLine, int consoleWidth, int tabWidth, char columnSeparator, int maxColumns,
            int columnPadding, long maxBufferedLines, boolean flushOnTab, WhiteSpaceHandler whiteSpaceHandler) {
        super(out, autoFlush && !autoColumn);
        this.autoColumn = autoColumn;
        this.autoCrop = autoCrop;
        this.appendToLongLine = appendToLongLine;
        this.consoleWidth = consoleWidth;
        this.tabWidth = tabWidth;
        this.columnSeparator = columnSeparator;
        this.maxColumns = maxColumns;
        this.maxBufferedLines = maxBufferedLines;
        this.columnPadding = columnPadding;
        this.flushOnTab = flushOnTab;
        this.whiteSpaceHandler = whiteSpaceHandler;

        this.buffer = new ArrayList<Line>();
        this.formatString = new StringBuilder();
        this.formatter = new Formatter(this.formatString);

        this.colorMode = null;
        this.colorForeground = null;
        this.colorBackground = null;

        this.tabLevel = 0;
        this.tabs = "";

        this.buffer.add(new Line());
    }

    public void setTabLevel(int level) {
        this.tabLevel = level;
        this.tabs = Strings.repeat(" ", tabWidth * level);
        if (flushOnTab)
            flushColumns();
    }

    public void incrementTabLevel() {
        setTabLevel(tabLevel + 1);
    }

    public void decrementTabLevel() {
        if (tabLevel == 0) {
            return;
        }

        setTabLevel(tabLevel - 1);
    }

    private int determineNumColumns() {
        int max = 0;
        for (Line line : buffer) {
            int num = line.countCharacter(columnSeparator);
            if (num > max) {
                max = num;
            }
        }

        return max > maxColumns ? maxColumns : max;
    }

    private int[] determineColumnWidths() {
        int columns = determineNumColumns();
        if (columns == 0) {
            return null;
        }

        int[] widths = new int[columns];
        for (Line line : buffer) {
            for (int last = 0, idx = 0; last < line.length() && idx < columns; ++idx) {
                int pos = line.indexOf(columnSeparator, last);
                if (pos < 0)
                    break;

                int wid = pos - last + 1 + columnPadding;
                if (wid > widths[idx]) {
                    widths[idx] = wid;
                }

                last = line.firstNonWhiteSpace(idx + 1);
            }
        }

        return widths;
    }

    private Line toColumns(int[] widths, Line line) throws IOException {
        int last = 0;
        for (int i = 0; i < widths.length; ++i) {
            int width = widths[i];

            int idx = line.indexOf(columnSeparator, last);
            if (idx < 0)
                break;

            if ((idx + 1) <= width) {
                line.spaceOut(width - (idx + 1), idx + 1);
            }

            last = line.firstNonWhiteSpace(idx + 1);
        }

        return line;
    }

    public void flushColumns() {
        flushColumns(false);
    }

    private void flushColumns(boolean preserveLast) {
        int size = buffer.size();

        int[] widths = null;
        if (autoColumn) {
            widths = determineColumnWidths();
        }

        StringBuilder builder = new StringBuilder();

        try {
            for (int i = 0; i < size - 1; ++i) {
                Line line = buffer.get(i);
                if (widths != null) {
                    line = toColumns(widths, line);
                }

                fixupLine(line);
                builder.setLength(0);
                line.toString(builder);

                super.out.append(builder.toString());
                super.out.append(LINE_SEP);
            }

            if (!preserveLast) {
                Line line = buffer.get(size - 1);
                if (widths != null) {
                    line = toColumns(widths, line);
                }

                fixupLine(line);
                builder.setLength(0);
                line.toString(builder);
                super.out.append(builder.toString());
            }

            super.out.flush();
        } catch (IOException ex) {
        }

        Line addback = null;
        if (preserveLast) {
            addback = buffer.get(size - 1);
        }

        buffer.clear();
        if (addback != null)
            buffer.add(addback);
        else
            buffer.add(new Line());
    }

    private void flushIfNeeded() {
        flushIfNeeded(false);
    }

    private void flushIfNeeded(boolean preserveLast) {
        if (!autoColumn || buffer.size() > maxBufferedLines) {
            flushColumns(preserveLast);
        }
    }

    private void appendToCurrent(String s) {
        int size = buffer.size();
        Line value = buffer.get(size - 1);
        if (value.isEmpty()) {
            value.append(tabs());
        }

        value.append(span(s));
    }

    private void fixupLine(Line line) {
        if (autoCrop) {
            line.trimTo(consoleWidth, appendToLongLine);
        }
    }

    private void print(String s, boolean mayHaveNewlines) {
        if (s == null) {
            appendToCurrent("null");
            return;
        }

        if (s.isEmpty()) {
            return;
        }

        if (LINE_SEP.equals(s)) {
            buffer.add(new Line());
            flushIfNeeded();
            return;
        }

        if (whiteSpaceHandler != null) {
            boolean endswith = s.endsWith(LINE_SEP);
            switch (whiteSpaceHandler) {
            case ELIMINATE_NEWLINES:
                s = s.replaceAll("\\r\\n|\\r|\\n", " ");
                break;

            case COLLAPSE_WHITESPACE:
                s = s.replaceAll("\\s+", " ");
                break;
            }

            mayHaveNewlines = endswith;
            if (endswith)
                s = s + LINE_SEP;
        }

        if (!mayHaveNewlines) {
            appendToCurrent(s);
            return;
        }

        String lines[] = s.split("\\r?\\n", -1);
        appendToCurrent(lines[0]);

        for (int i = 1; i < lines.length; ++i) {
            String value = lines[i];
            if (value.isEmpty()) {
                buffer.add(new Line());
            } else {
                Line line = new Line();
                line.append(tabs());
                line.append(span(value, true));
                buffer.add(line);
            }
        }

        resetColor();
        flushIfNeeded(true);
    }

    @Override
    public void print(String s) {
        print(s, true);
    }

    @Override
    public void println() {
        print(LINE_SEP, true);
        flushIfNeeded();
    }

    @Override
    public void println(String x) {
        print(x);
        println();
    }

    @Override
    public void print(boolean b) {
        print(String.valueOf(b), false);
    }

    @Override
    public void print(char c) {
        print(String.valueOf(c), false);
    }

    @Override
    public void print(int i) {
        print(String.valueOf(i), false);
    }

    @Override
    public void print(long l) {
        print(String.valueOf(l), false);
    }

    @Override
    public void print(float f) {
        print(String.valueOf(f), false);
    }

    @Override
    public void print(double d) {
        print(String.valueOf(d), false);
    }

    @Override
    public void print(char[] s) {
        print(String.valueOf(s), true);
    }

    @Override
    public void print(Object obj) {
        print(String.valueOf(obj), true);
    }

    @Override
    public PrintWriter printf(String format, Object... args) {
        return printf(formatter.locale(), format, args);
    }

    @Override
    public PrintWriter printf(Locale l, String format, Object... args) {
        formatter.format(l, format, args);

        String results = formatString.toString();
        formatString.setLength(0);

        print(results);
        flushIfNeeded();
        return this;
    }

    @Override
    public PrintWriter format(String format, Object... args) {
        return printf(format, args);
    }

    @Override
    public PrintWriter format(Locale l, String format, Object... args) {
        return printf(l, format, args);
    }

    @Override
    public PrintWriter append(char c) {
        print(c);
        return this;
    }

    @Override
    public PrintWriter append(CharSequence csq) {
        if (csq == null) {
            print("null");
            return this;
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public PrintWriter append(CharSequence csq, int start, int end) {
        if (csq == null) {
            print("null");
            return this;
        }
        print(csq.subSequence(start, end).toString());
        return this;
    }

    @Override
    public void println(boolean x) {
        print(x);
        println();
    }

    @Override
    public void println(char x) {
        print(x);
        println();
    }

    @Override
    public void println(int x) {
        print(x);
        println();
    }

    @Override
    public void println(long x) {
        print(x);
        println();
    }

    @Override
    public void println(float x) {
        print(x);
        println();
    }

    @Override
    public void println(double x) {
        print(x);
        println();
    }

    @Override
    public void println(char[] x) {
        print(x);
        println();
    }

    @Override
    public void println(Object x) {
        print(x);
        println();
    }

    public void rule(char c) {
        if (tabs.length() >= consoleWidth)
            return;

        int width = consoleWidth;
        if (width == Integer.MAX_VALUE) {
            width = 100;
        }

        println(Strings.repeat(String.valueOf(c), width - tabs.length()));
    }

    public boolean acceptColorModification = true;

    public PrettyPrintWriter iff(boolean predicate) {
        if (!predicate && acceptColorModification) {
            resetColor();
        } else {
            acceptColorModification = false;
        }

        return this;
    }

    public PrettyPrintWriter otherwise() {
        acceptColorModification = false;
        return this;
    }

    public PrettyPrintWriter black() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_BLACK;
        return this;
    }

    public PrettyPrintWriter red() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_RED;
        return this;
    }

    public PrettyPrintWriter green() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_GREEN;
        return this;
    }

    public PrettyPrintWriter yellow() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_YELLOW;
        return this;
    }

    public PrettyPrintWriter blue() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_BLUE;
        return this;
    }

    public PrettyPrintWriter magenta() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_MAGENTA;
        return this;
    }

    public PrettyPrintWriter cyan() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_CYAN;
        return this;
    }

    public PrettyPrintWriter white() {
        if (!acceptColorModification)
            return this;
        colorForeground = FG_COLOR_WHITE;
        return this;
    }

    public PrettyPrintWriter bgblack() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_BLACK;
        return this;
    }

    public PrettyPrintWriter bgred() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_RED;
        return this;
    }

    public PrettyPrintWriter bggreen() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_GREEN;
        return this;
    }

    public PrettyPrintWriter bgyellow() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_YELLOW;
        return this;
    }

    public PrettyPrintWriter bgblue() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_BLUE;
        return this;
    }

    public PrettyPrintWriter bgmagenta() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_MAGENTA;
        return this;
    }

    public PrettyPrintWriter bgcyan() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_CYAN;
        return this;
    }

    public PrettyPrintWriter bgwhite() {
        if (!acceptColorModification)
            return this;
        colorBackground = BG_COLOR_WHITE;
        return this;
    }

    public PrettyPrintWriter bold() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_BOLD;
        return this;
    }

    public PrettyPrintWriter blink() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_BLINK;
        return this;
    }

    public PrettyPrintWriter concealed() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_CONCEALED;
        return this;
    }

    public PrettyPrintWriter off() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_OFF;
        return this;
    }

    public PrettyPrintWriter underscore() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_UNDER;
        return this;
    }

    public PrettyPrintWriter reverse() {
        if (!acceptColorModification)
            return this;
        colorMode = MODE_REVERSE;
        return this;
    }

    public static Builder stdoutPrettyPrinter() {
        return new Builder(Main.out).withAutoFlush();
    }

    public static Builder stderrPrettyPrinter() {
        return new Builder(Main.err).withAutoFlush();
    }

    public static Builder newPrettyPrinter(OutputStream out) {
        return new Builder(out);
    }

    public static final class Builder {
        private final OutputStream out;
        private boolean autoFlush;

        private boolean autoColumn;
        private char columnSeparator;
        private int maxColumns;
        private int columnPadding;
        private long maxBufferedLines;

        private boolean autoCrop;
        private int consoleWidth;
        private Span appendToLongLine;

        private int tabWidth;
        private boolean flushOnTab;
        private WhiteSpaceHandler whiteSpaceHandler;

        public Builder(OutputStream out) {
            this.out = out;
            this.autoFlush = false;
            this.autoColumn = false;
            this.flushOnTab = false;
            this.columnSeparator = DEFAULT_COLUMN_SEP;
            this.maxColumns = DEFAULT_MAX_COLUMNS;
            this.columnPadding = DEFAULT_COLUMN_PADDING;
            this.autoCrop = false;
            this.consoleWidth = DEFAULT_WIDTH;
            this.appendToLongLine = null;
            this.tabWidth = DEFAULT_TABS;
            this.whiteSpaceHandler = null;
            this.maxBufferedLines = Long.MAX_VALUE;
        }

        public Builder withAutoFlush() {
            this.autoFlush = true;
            return this;
        }

        public Builder withAutoCrop() {
            return withAutoCrop(DEFAULT_WIDTH);
        }

        public Builder withAutoCrop(int consoleWidth) {
            return withAutoCrop(consoleWidth, DEFAULT_APPEND);
        }

        public Builder withAutoCrop(int consoleWidth, String appendToLong) {
            return withAutoCrop(consoleWidth, mkspan(appendToLong));
        }

        public Builder withAutoCrop(int consoleWidth, Span appendToLong) {
            this.consoleWidth = consoleWidth;
            this.appendToLongLine = appendToLong;
            this.autoCrop = true;
            return this;
        }

        public Builder withTabSize(int tabWidth) {
            this.tabWidth = tabWidth;
            return this;
        }

        public Builder withAutoColumn() {
            return withAutoColumn(DEFAULT_COLUMN_SEP);
        }

        public Builder withAutoColumn(char columnSeparator) {
            return withAutoColumn(columnSeparator, DEFAULT_MAX_COLUMNS);
        }

        public Builder withAutoColumn(char columnSeparator, int maxColumns) {
            this.autoColumn = true;
            this.columnSeparator = columnSeparator;
            this.maxColumns = maxColumns;
            return this;
        }

        public Builder withColumnPadding(int columnPadding) {
            this.columnPadding = columnPadding;
            return this;
        }

        public Builder withWhitespaceHandler(WhiteSpaceHandler whiteSpaceHandler) {
            this.whiteSpaceHandler = whiteSpaceHandler;
            return this;
        }

        public Builder withMaxBufferedLines(long maxBufferedLines) {
            this.maxBufferedLines = maxBufferedLines;
            return this;
        }

        public Builder withFlushOnTab() {
            this.flushOnTab = true;
            return this;
        }

        public PrettyPrintWriter build() {
            return new PrettyPrintWriter(out, autoFlush, autoColumn, autoCrop, appendToLongLine, consoleWidth,
                    tabWidth, columnSeparator, maxColumns, columnPadding, maxBufferedLines, flushOnTab,
                    whiteSpaceHandler);
        }
    }

    private Span tabs() {
        return new Span(tabs);
    }

    private Span span(String span) {
        return span(span, false);
    }

    private void resetColor() {
        acceptColorModification = true;

        colorMode = null;
        colorForeground = null;
        colorBackground = null;
    }

    public static Span mkspan(String span) {
        return new Span(span);
    }

    public static Span mkspan(String span, String color) {
        return mkspan(span, null, color, null);
    }

    public static Span mkspan(String span, String colorMode, String colorForeground, String colorBackground) {
        if (DEFAULT_COLORS > 0 && (colorMode != null || colorForeground != null || colorBackground != null)) {
            String color = "\u001B[" + Joiner.on(';').skipNulls().join(colorMode, colorForeground, colorBackground)
                    + "m";
            return new Span(span, color);
        } else {
            return mkspan(span);
        }
    }

    private Span span(String span, boolean keepColor) {
        Span result;
        if (DEFAULT_COLORS > 0 && (colorMode != null || colorForeground != null || colorBackground != null)) {
            result = mkspan(span, colorMode, colorForeground, colorBackground);
        } else {
            result = mkspan(span);
        }

        if (!keepColor) {
            resetColor();
        }

        return result;
    }

    public static final class Line {
        private List<Span> spans;
        private int length;

        public Line() {
            this.spans = new ArrayList<Span>();
        }

        public void append(Span span) {
            length += span.length();
            if (spans.isEmpty()) {
                spans.add(span);
                return;
            }

            Span last = spans.get(spans.size() - 1);
            if (last.canAppend(span)) {
                last.append(span);
            } else {
                spans.add(span);
            }
        }

        public boolean isEmpty() {
            return length == 0;
        }

        public int length() {
            return length;
        }

        public int indexOf(char ch, int start) {
            int offset = 0;
            for (Span span : spans) {
                if (start > span.length()) {
                    start -= span.length();
                    continue;
                }

                int idx = span.indexOf(ch, start);
                if (idx >= 0)
                    return offset + idx;

                offset += span.length() - start;
                start = 0;
            }

            return -1;
        }

        public void spaceOut(int width, int start) {
            for (Span span : spans) {
                if (start > span.length()) {
                    start -= span.length();
                    continue;
                }

                span.spaceOut(width, start);
                return;
            }
        }

        public int firstNonWhiteSpace(int start) {
            return start;
        }

        public int countCharacter(char ch) {
            int result = 0;
            for (Span span : spans) {
                result += span.countCharacter(ch);
            }

            return result;
        }

        public void trimTo(int width, Span appendToLongLine) {
            int i = 0;
            int remaining = width;
            for (i = 0; i < spans.size(); ++i) {
                Span next = spans.get(i);
                if (next.length() > remaining) {
                    ++i;
                    next.trimTo(remaining, appendToLongLine);
                    break;
                }

                remaining -= next.length();
            }

            for (; i < spans.size(); ++i) {
                spans.remove(i);
            }
        }

        public void toString(StringBuilder builder) {
            for (Span span : spans) {
                span.toString(builder);
            }
        }
    }

    public static final class Span {
        private String span;
        private final String color;

        public Span(String span) {
            this(span, null);
        }

        public Span(String span, String color) {
            this.span = span;
            this.color = color;
        }

        public int length() {
            return span.length();
        }

        public boolean isEmpty() {
            return span.isEmpty();
        }

        public int indexOf(char ch, int start) {
            return span.indexOf(ch, start);
        }

        public void spaceOut(int width, int start) {
            int removeTo = start;
            while (removeTo < span.length() && Character.isWhitespace(span.charAt(removeTo))) {
                removeTo++;
            }

            span = span.substring(0, start) + Strings.repeat(" ", width) + span.substring(removeTo);
        }

        public int countCharacter(char ch) {
            int result = 0;
            for (int i = 0; i < span.length(); ++i) {
                if (span.charAt(i) == ch) {
                    result++;
                }
            }

            return result;
        }

        public void trimTo(int width, Span appendToLongLine) {
            if (appendToLongLine != null && !appendToLongLine.isEmpty()) {
                int shortten = appendToLongLine.length();
                if (shortten > width)
                    shortten = width;

                span = span.substring(0, width - shortten) + appendToLongLine;
            } else {
                span = span.substring(0, width + 1);
            }
        }

        public String toString() {
            StringBuilder builder = new StringBuilder();
            toString(builder);
            return builder.toString();
        }

        public void toString(StringBuilder builder) {
            if (color != null)
                builder.append(color);
            builder.append(span);
            if (color != null)
                builder.append(RESET);
        }

        public void append(Span other) {
            span = span + other.span;
        }

        public boolean canAppend(Span other) {
            if (color == null && other == null)
                return true;
            if (color == null && other != null)
                return false;
            return color.equals(other);
        }
    }
}