All Downloads are FREE. Search and download functionalities are using the official Maven repository.

parquet.tools.util.PrettyPrintWriter Maven / Gradle / Ivy

There is a newer version: 1.6.0
Show newest version
/**
 * 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 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();
    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 spans;
    private int length;

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

    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);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy