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

com.vladsch.flexmark.util.format.MarkdownTable Maven / Gradle / Ivy

The newest version!
package com.vladsch.flexmark.util.format;

import static com.vladsch.flexmark.util.format.TableCell.DEFAULT_CELL;
import static com.vladsch.flexmark.util.format.TableCell.NOT_TRACKED;
import static com.vladsch.flexmark.util.format.options.DiscretionaryText.ADD;
import static com.vladsch.flexmark.util.misc.Utils.compare;
import static com.vladsch.flexmark.util.misc.Utils.max;
import static com.vladsch.flexmark.util.misc.Utils.maxLimit;
import static com.vladsch.flexmark.util.misc.Utils.min;
import static com.vladsch.flexmark.util.misc.Utils.minLimit;
import static com.vladsch.flexmark.util.misc.Utils.rangeLimit;

import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
import com.vladsch.flexmark.util.collection.MaxAggregator;
import com.vladsch.flexmark.util.collection.MinAggregator;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.format.options.DiscretionaryText;
import com.vladsch.flexmark.util.html.CellAlignment;
import com.vladsch.flexmark.util.misc.ArrayUtils;
import com.vladsch.flexmark.util.misc.CharPredicate;
import com.vladsch.flexmark.util.misc.Pair;
import com.vladsch.flexmark.util.misc.Ref;
import com.vladsch.flexmark.util.misc.Utils;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import com.vladsch.flexmark.util.sequence.LineAppendable;
import com.vladsch.flexmark.util.sequence.PrefixedSubSequence;
import com.vladsch.flexmark.util.sequence.RepeatedSequence;
import com.vladsch.flexmark.util.sequence.SequenceUtils;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.function.BinaryOperator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class MarkdownTable {
  public final TableSection header;
  public final TableSection separator;
  public final TableSection body;
  public final TableSection caption;
  public TableFormatOptions options;

  private boolean isHeading;
  private boolean isSeparator;
  CharSequence formatTableIndentPrefix;

  // used by finalization and conversion to text
  private CellAlignment[] alignments;
  private int[] columnWidths;

  // generated by conversion to text
  private final @NotNull ArrayList trackedOffsets = new ArrayList<>();

  private final TableSection[] allSections; // includes  header, separator, body, caption
  private final TableSection[] allTableRows; // includes header, separator, body
  private final TableSection[] allContentRows; // header, body
  private final TableSection[] allHeaderRows; // header
  private final TableSection[] allBodyRows; // body
  public static final CharPredicate COLON_TRIM_CHARS = CharPredicate.anyOf(':');
  private final CharSequence tableChars;

  public static final NumericSuffixPredicate NO_SUFFIXES = s -> false;
  public static final NumericSuffixPredicate ALL_SUFFIXES_SORT = s -> true;
  public static final NumericSuffixPredicate ALL_SUFFIXES_NO_SORT =
      new NumericSuffixPredicate() {
        @Override
        public boolean test(String s) {
          return true;
        }

        @Override
        public boolean sortSuffix(@NotNull String suffix) {
          return false;
        }
      };

  public MarkdownTable(@NotNull CharSequence tableChars, @Nullable DataHolder options) {
    this(tableChars, new TableFormatOptions(options));
  }

  public MarkdownTable(@NotNull CharSequence tableChars, @Nullable TableFormatOptions options) {
    this.tableChars = tableChars;
    this.formatTableIndentPrefix = options == null ? "" : options.formatTableIndentPrefix;
    header = new TableSection(TableSectionType.HEADER);
    separator = new TableSeparatorSection(TableSectionType.SEPARATOR);
    body = new TableSection(TableSectionType.BODY);
    caption = new TableCaptionSection(TableSectionType.CAPTION);
    isHeading = true;
    isSeparator = false;
    this.options = options == null ? new TableFormatOptions(null) : options;

    allSections = new TableSection[] {header, separator, body, caption};
    allTableRows = new TableSection[] {header, separator, body};
    allContentRows = new TableSection[] {header, body};
    allHeaderRows = new TableSection[] {header};
    allBodyRows = new TableSection[] {body};
  }

  public CharSequence getTableChars() {
    return tableChars;
  }

  public TableCell getCaptionCell() {
    return !caption.rows.isEmpty() && !caption.rows.get(0).cells.isEmpty()
        ? caption.rows.get(0).cells.get(0)
        : TableCaptionSection.NULL_CELL;
  }

  public CharSequence getFormatTableIndentPrefix() {
    return formatTableIndentPrefix;
  }

  public void setFormatTableIndentPrefix(CharSequence formatTableIndentPrefix) {
    this.formatTableIndentPrefix = formatTableIndentPrefix;
  }

  public void setCaptionCell(TableCell captionCell) {
    if (caption.rows.isEmpty()) {
      caption.rows.add(caption.defaultRow());
    }

    caption.rows.get(0).cells.clear();
    caption.rows.get(0).cells.add(captionCell);
  }

  public BasedSequence getCaption() {
    return getCaptionCell().text;
  }

  public void setCaption(CharSequence caption) {
    TableCell captionCell = getCaptionCell();
    setCaptionCell(
        captionCell.withText(
            captionCell.openMarker.isEmpty() ? "[" : captionCell.openMarker,
            caption,
            captionCell.closeMarker.isEmpty() ? "]" : captionCell.closeMarker));
  }

  /*
   * Used by visitor during table creation
   *
   */
  public void setCaptionWithMarkers(
      Node tableCellNode,
      CharSequence captionOpen,
      CharSequence caption,
      CharSequence captionClose) {
    setCaptionCell(
        new TableCell(
            tableCellNode,
            captionOpen,
            options.formatTableCaptionSpaces == DiscretionaryText.AS_IS
                ? caption
                : BasedSequence.of(caption).trim(),
            captionClose,
            1,
            1));
  }

  public int getHeadingRowCount() {
    return header.rows.size();
  }

  public int getSeparatorRowCount() {
    return separator.rows.size();
  }

  public int getBodyRowCount() {
    return body.rows.size();
  }

  public int getCaptionRowCount() {
    return caption.rows.size();
  }

  public int getMaxHeadingColumns() {
    return header.getMaxColumns();
  }

  public int getMaxSeparatorColumns() {
    return separator.getMaxColumns();
  }

  public int getMaxBodyColumns() {
    return body.getMaxColumns();
  }

  public boolean getHaveCaption() {
    return !caption.rows.isEmpty()
        && !caption.rows.get(0).cells.isEmpty()
        && caption.rows.get(0).cells.get(0).columnSpan != 0;
  }

  public int getMinColumns() {
    int headingColumns = header.getMinColumns();
    int separatorColumns = separator.getMinColumns();
    int bodyColumns = body.getMinColumns();
    return min(
        headingColumns == 0 ? Integer.MAX_VALUE : headingColumns,
        separatorColumns,
        bodyColumns == 0 ? Integer.MAX_VALUE : bodyColumns);
  }

  public int getMaxColumns() {
    int headingColumns = header.getMaxColumns();
    int separatorColumns = separator.getMaxColumns();
    int bodyColumns = body.getMaxColumns();
    return max(headingColumns, separatorColumns, bodyColumns);
  }

  public int getMinColumnsWithoutColumns(boolean withSeparator, int... skipColumns) {
    return aggregateTotalColumnsWithoutColumns(
        withSeparator ? allTableRows : allContentRows, MinAggregator.INSTANCE, skipColumns);
  }

  public int getMaxColumnsWithoutColumns(boolean withSeparator, int... skipColumns) {
    return aggregateTotalColumnsWithoutColumns(
        withSeparator ? allTableRows : allContentRows, MaxAggregator.INSTANCE, skipColumns);
  }

  public int getMinColumnsWithoutRows(boolean withSeparator, int... skipRows) {
    return aggregateTotalColumnsWithoutRows(
        withSeparator ? allTableRows : allContentRows, MinAggregator.INSTANCE, skipRows);
  }

  public int getMaxColumnsWithoutRows(boolean withSeparator, int... skipRows) {
    return aggregateTotalColumnsWithoutRows(
        withSeparator ? allTableRows : allContentRows, MaxAggregator.INSTANCE, skipRows);
  }

  @NotNull
  public List getTrackedOffsets() {
    return trackedOffsets;
  }

  @Nullable
  private TrackedOffset findTrackedOffset(int offset) {
    for (TrackedOffset trackedOffset : trackedOffsets) {
      if (trackedOffset.getOffset() == offset) {
        return trackedOffset;
      }
      if (trackedOffset.getOffset() > offset) {
        break;
      }
    }
    return null;
  }

  @Nullable
  public TrackedOffset getTrackedOffset(int offset) {
    return findTrackedOffset(offset);
  }

  public int getTrackedOffsetIndex(int offset) {
    TrackedOffset trackedOffset = findTrackedOffset(offset);
    return trackedOffset == null ? offset : trackedOffset.getIndex();
  }

  public int getTableStartOffset() {
    // MdNav:diagnostic/4134, index out of range
    List rows = getAllRows();
    if (!rows.isEmpty()) {
      TableRow row = rows.get(0);
      row.normalizeIfNeeded();

      if (!row.cells.isEmpty()) {
        return row.cells.get(0).getStartOffset(null);
      }
    }
    return 0;
  }

  public TableCellOffsetInfo getCellOffsetInfo(int offset) {
    int r = 0;
    for (TableRow row : getAllSectionRows()) {
      row.normalizeIfNeeded();
      TableCell lastCell = row.cells.get(row.cells.size() - 1);
      BasedSequence lastSegment = lastCell.getLastSegment();
      int lineEndOffset = lastSegment.baseEndOfLineAnyEOL();
      if (lineEndOffset == -1) lineEndOffset = lastSegment.getEndOffset();

      if (offset <= lineEndOffset) {
        // it is on this line

        int i = 0;
        TableCell previousCell = null;
        for (TableCell cell : row.cells) {
          if (!cell.closeMarker.isEmpty()
              ? offset < cell.closeMarker.getEndOffset()
              : offset <= cell.text.getEndOffset()) {
            if (offset >= cell.getInsideStartOffset(previousCell)
                && offset <= cell.getInsideEndOffset()) {
              // in the cell area
              return new TableCellOffsetInfo(
                  offset,
                  this,
                  getAllRowsSection(r),
                  row,
                  cell,
                  r,
                  i,
                  i,
                  offset - cell.getInsideStartOffset(previousCell));
            }
            // it the span area or before pipe of first cell
            return new TableCellOffsetInfo(
                offset, this, getAllRowsSection(r), row, cell, r, i, null, null);
          }
          i++;
          previousCell = cell;
        }
        // after the last cell
        return new TableCellOffsetInfo(
            offset, this, getAllRowsSection(r), row, lastCell, r, i, null, null);
      }
      r++;
    }

    TableSection lastSection = getAllRowsSection(r - 1);
    return new TableCellOffsetInfo(offset, this, lastSection, null, null, r, 0, null, null);
  }

  /**
   * @deprecated Use {@link #addTrackedOffset(TrackedOffset)} To create: TrackedOffset.track(offset)
   */
  @Deprecated
  public boolean addTrackedOffset(int offset) {
    return addTrackedOffset(TrackedOffset.track(offset, null, false));
  }

  /**
   * @deprecated Use {@link #addTrackedOffset(TrackedOffset)} To create: TrackedOffset.track(offset,
   *     afterSpace)
   */
  @Deprecated
  public boolean addTrackedOffset(int offset, boolean afterSpace) {
    return addTrackedOffset(TrackedOffset.track(offset, afterSpace ? ' ' : null, false));
  }

  /**
   * @deprecated Use {@link #addTrackedOffset(TrackedOffset)} To create: TrackedOffset.track(offset,
   *     afterSpace, afterDelete)
   */
  @Deprecated
  public boolean addTrackedOffset(int offset, boolean afterSpace, boolean afterDelete) {
    return addTrackedOffset(TrackedOffset.track(offset, afterSpace ? ' ' : null, afterDelete));
  }

  /**
   * @deprecated Use {@link #addTrackedOffset(TrackedOffset)} To create: TrackedOffset.track(offset,
   *     c, afterDelete)
   */
  @Deprecated
  public boolean addTrackedOffset(int offset, Character c, boolean afterDelete) {
    return addTrackedOffset(TrackedOffset.track(offset, c, afterDelete));
  }

  public boolean addTrackedOffset(@NotNull TrackedOffset trackedOffset) {
    int offset = trackedOffset.getOffset();
    trackedOffsets.removeIf(it -> it.getOffset() == offset);
    trackedOffsets.add(trackedOffset);

    TableCellOffsetInfo info = getCellOffsetInfo(offset);
    if (info.getInsideColumn()) {
      // real cell, we can add it to the cells contents
      info.tableRow.cells.set(
          info.column,
          info.tableCell.withTrackedOffset(
              offset
                  - info.tableCell.getTextStartOffset(
                      info.column == 0 ? null : info.tableRow.cells.get(info.column - 1)),
              trackedOffset.isAfterSpaceEdit(),
              trackedOffset.isAfterDelete()));
      return true;
    } else if (info.isBeforeCells()) {
      // in the before span
      // we will add it as inside the cell???? since we don't have a before span
      info.tableRow.setBeforeOffset(offset);
      return true;
    } else if (info.isInCellSpan()) {
      // in the after span
      info.tableRow.cells.set(
          info.column,
          info.tableCell.withSpanTrackedOffset(offset - info.tableCell.getInsideEndOffset()));
      return true;
    } else if (info.isAfterCells()) {
      // must be after the row, can go after the row.
      info.tableRow.setAfterOffset(offset);
      return true;
    }

    return false;
  }

  /*
      Table Manipulation Helper API
  */
  public List getAllRows() {
    return getAllSectionsRows(allTableRows);
  }

  public List getAllContentRows() {
    return getAllSectionsRows(allContentRows);
  }

  public List getAllSectionRows() {
    return getAllSectionsRows(allSections);
  }

  private List getAllSectionsRows(TableSection... sections) {
    List rows = new ArrayList<>(header.rows.size() + body.rows.size());
    for (TableSection section : sections) {
      rows.addAll(section.rows);
    }
    return rows;
  }

  public boolean isAllRowsSeparator(int index) {
    return index >= header.rows.size() && index < header.rows.size() + separator.rows.size();
  }

  public TableSection getAllRowsSection(int index) {
    for (TableSection section : allSections) {
      if (index < section.rows.size()) {
        return section;
      }
      index -= section.rows.size();
    }
    return null;
  }

  public int getAllRowsCount() {
    return header.rows.size() + separator.rows.size() + body.rows.size();
  }

  public int getAllContentRowsCount() {
    return header.rows.size() + body.rows.size();
  }

  public int getAllSectionsRowsCount() {
    return header.rows.size() + separator.rows.size() + body.rows.size() + caption.rows.size();
  }

  public void forAllRows(TableRowManipulator manipulator) {
    forAllSectionsRows(0, Integer.MAX_VALUE, allTableRows, manipulator);
  }

  public void forAllRows(int startIndex, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, Integer.MAX_VALUE, allTableRows, manipulator);
  }

  public void forAllRows(int startIndex, int count, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, count, allTableRows, manipulator);
  }

  public void forAllContentRows(TableRowManipulator manipulator) {
    forAllSectionsRows(0, Integer.MAX_VALUE, allContentRows, manipulator);
  }

  public void forAllContentRows(int startIndex, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, Integer.MAX_VALUE, allContentRows, manipulator);
  }

  public void forAllContentRows(int startIndex, int count, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, count, allContentRows, manipulator);
  }

  public void forAllSectionRows(TableRowManipulator manipulator) {
    forAllSectionsRows(0, Integer.MAX_VALUE, allSections, manipulator);
  }

  public void forAllSectionRows(int startIndex, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, Integer.MAX_VALUE, allSections, manipulator);
  }

  public void forAllSectionRows(int startIndex, int count, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, count, allSections, manipulator);
  }

  public void forAllHeaderRows(TableRowManipulator manipulator) {
    forAllSectionsRows(0, Integer.MAX_VALUE, allHeaderRows, manipulator);
  }

  public void forAllHeaderRows(int startIndex, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, Integer.MAX_VALUE, allHeaderRows, manipulator);
  }

  public void forAllHeaderRows(int startIndex, int count, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, count, allHeaderRows, manipulator);
  }

  public void forAllBodyRows(TableRowManipulator manipulator) {
    forAllSectionsRows(0, Integer.MAX_VALUE, allBodyRows, manipulator);
  }

  public void forAllBodyRows(int startIndex, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, Integer.MAX_VALUE, allHeaderRows, manipulator);
  }

  public void forAllBodyRows(int startIndex, int count, TableRowManipulator manipulator) {
    forAllSectionsRows(startIndex, count, allHeaderRows, manipulator);
  }

  public void deleteRows(int rowIndex, int count) {
    if (rowIndex <= header.rows.size()) {
      int i = count;
      while (i-- > 0 && rowIndex < header.rows.size()) {
        header.rows.remove(rowIndex);
      }
    } else if (rowIndex >= header.rows.size() + separator.rows.size()) {
      int index = rowIndex - header.rows.size() - separator.rows.size();
      int i = count;
      while (i-- > 0 && index < body.rows.size()) {
        body.rows.remove(index);
      }
    }
  }

  public void insertRows(int rowIndex, int count) {
    int maxColumns = getMaxColumns();
    if (rowIndex <= header.rows.size()) {
      insertRows(header.rows, rowIndex, count, maxColumns);
    } else {
      insertRows(
          body.rows,
          rangeLimit(rowIndex - header.rows.size() - separator.rows.size(), 0, body.rows.size()),
          count,
          maxColumns);
    }
  }

  private static void insertRows(List rows, int index, int count, int maxColumns) {
    int i = count;
    while (i-- > 0) {
      TableRow emptyRow = new TableRow();
      emptyRow.appendColumns(maxColumns);
      if (index >= rows.size()) {
        rows.add(emptyRow);
      } else {
        rows.add(index, emptyRow);
      }
    }
  }

  public void insertColumns(int column, int count) {
    forAllContentRows(
        (row, allRowsIndex, rows, index) -> {
          rows.get(index).insertColumns(column, count);
          return 0;
        });

    // insert separator columns separately, since they are not reduced in finalize
    for (TableRow row : separator.rows) {
      row.insertColumns(column, count);
    }
  }

  public void deleteColumns(int column, int count) {
    forAllContentRows(
        (row, allRowsIndex, rows, index) -> {
          rows.get(index).deleteColumns(column, count);
          return 0;
        });

    // delete separator columns separately, since they are not reduced in finalize
    for (TableRow row : separator.rows) {
      row.deleteColumns(column, count);
    }
  }

  public void moveColumn(int fromColumn, int toColumn) {
    forAllContentRows(
        (row, allRowsIndex, rows, index) -> {
          rows.get(index).moveColumn(fromColumn, toColumn);
          return 0;
        });

    // move separator columns separately, since they are not reduced in finalize
    for (TableRow row : separator.rows) {
      row.moveColumn(fromColumn, toColumn);
    }
  }

  /**
   * Test all rows for having given column empty. All columns after row's max column are empty
   *
   * @param column index in allRows list
   * @return true if column is empty for all rows, separator row excluded
   */
  public boolean isEmptyColumn(int column) {
    boolean[] result = new boolean[] {true};
    forAllContentRows(
        (row, allRowsIndex, rows, index) -> {
          if (!row.isEmptyColumn(column)) {
            result[0] = false;
            return TableRowManipulator.BREAK;
          }
          return 0;
        });

    return result[0];
  }

  /**
   * Test a row for having all empty columns
   *
   * @param rowIndex index in allRows list
   * @return true if row is empty or is a separator row
   */
  public boolean isAllRowsEmptyAt(int rowIndex) {
    return isEmptyRowAt(rowIndex, allTableRows);
  }

  /**
   * Test a row for having all empty columns
   *
   * @param rowIndex index in allRows list
   * @return true if row is empty or is a separator row
   */
  public boolean isContentRowsEmptyAt(int rowIndex) {
    return isEmptyRowAt(rowIndex, allContentRows);
  }

  /**
   * Test a row for having all empty columns
   *
   * @param rowIndex index in allRows list
   * @param sections sections to use for rows array generation
   * @return true if row is empty or is a separator row
   */
  private static boolean isEmptyRowAt(int rowIndex, TableSection[] sections) {
    boolean[] result = new boolean[] {false};
    forAllSectionsRows(
        rowIndex,
        1,
        sections,
        (row, allRowsIndex, rows, index) -> {
          if (row.isEmpty()) {
            result[0] = true;
          }
          return TableRowManipulator.BREAK;
        });

    return result[0];
  }

  /*
   * Used during table construction by building the table
   * as the AST visiting process (Flexmark or HTML)
   *
   */

  public boolean getHeader() {
    return isHeading;
  }

  public void setHeader(boolean header) {
    isHeading = header;
  }

  public boolean isSeparator() {
    return isSeparator;
  }

  public void setSeparator(boolean separator) {
    isSeparator = separator;
  }

  public void setHeader() {
    isHeading = true;
    isSeparator = false;
  }

  public void setSeparator() {
    isSeparator = true;
    isHeading = false;
  }

  public void setBody() {
    isSeparator = false;
    isHeading = false;
  }

  public void nextRow() {
    if (isSeparator) {
      throw new IllegalStateException("Only one separator row allowed");
    }
    if (isHeading) {
      header.nextRow();
    } else {
      body.nextRow();
    }
  }

  /**
   * @param cell cell to add
   */
  public void addCell(@NotNull TableCell cell) {
    TableSection tableSection = isSeparator ? separator : isHeading ? header : body;

    if (isSeparator && (cell.columnSpan != 1 || cell.rowSpan != 1)) {
      throw new IllegalStateException("Separator columns cannot span rows/columns");
    }

    TableRow currentRow = tableSection.get(tableSection.row);

    // skip cells that are already set
    while (tableSection.column < currentRow.cells.size()
        && currentRow.cells.get(tableSection.column) != null) {
      tableSection.column++;
    }

    int rowSpan = 0;
    while (rowSpan < cell.rowSpan) {
      tableSection.get(tableSection.row + rowSpan).set(tableSection.column, cell);

      // set the rest to NULL cells up to null column
      int columnSpan = 1;
      while (columnSpan < cell.columnSpan) {
        tableSection.expandTo(tableSection.row + rowSpan, tableSection.column + columnSpan);
        if (tableSection.get(tableSection.row + rowSpan).cells.get(tableSection.column + columnSpan)
            != null) {
          break;
        }

        tableSection
            .rows
            .get(tableSection.row + rowSpan)
            .set(tableSection.column + columnSpan, TableCell.NULL);
        columnSpan++;
      }
      rowSpan++;
    }

    tableSection.column += cell.columnSpan;
  }

  public void normalize() {
    header.normalize();
    separator.normalize();
    body.normalize();
  }

  public void finalizeTable() {
    // remove null cells
    normalize();

    if (options.fillMissingColumns) {
      fillMissingColumns(options.formatTableFillMissingMinColumn);
    }

    int sepColumns = getMaxColumns();
    alignments = new CellAlignment[sepColumns];
    columnWidths = new int[sepColumns];
    BitSet spanAlignment = new BitSet(sepColumns);
    List columnSpans = new ArrayList<>();
    Ref delta = new Ref<>(0);

    if (!separator.rows.isEmpty()) {
      TableRow row = separator.rows.get(0);
      int jSpan = 0;
      delta.value = 0;
      for (TableCell cell : row.cells) {
        // set alignment if not already set or was set by a span and this column is not a span
        if ((alignments[jSpan] == null || cell.columnSpan == 1 && spanAlignment.get(jSpan))
            && cell.alignment != CellAlignment.NONE) {
          alignments[jSpan] = cell.alignment;
          if (cell.columnSpan > 1) spanAlignment.set(jSpan);
        }
        jSpan += cell.columnSpan;
      }
    }

    if (!header.rows.isEmpty()) {
      for (TableRow row : header.rows) {
        int j = 0;
        int jSpan = 0;
        int kMax = row.cells.size();
        for (int k = 0; k < kMax; k++) {
          TableCell cell = row.cells.get(k);

          // set alignment if not already set or was set by a span and this column is not a span
          if ((alignments[jSpan] == null || cell.columnSpan == 1 && spanAlignment.get(jSpan))
              && cell.alignment != CellAlignment.NONE) {
            alignments[jSpan] = cell.alignment;
            if (cell.columnSpan > 1) spanAlignment.set(jSpan);
          }

          delta.value = 0;
          BasedSequence cellText = cellText(row.cells, k, false, true, 0, null, delta);
          int width =
              options.charWidthProvider.getStringWidth(cellText)
                  + options.spacePad
                  + options.pipeWidth * cell.columnSpan;
          if (cell.columnSpan > 1) {
            columnSpans.add(new ColumnSpan(j, cell.columnSpan, width));
          } else {
            if (columnWidths[jSpan] < width) columnWidths[jSpan] = width;
          }

          j++;
          jSpan += cell.columnSpan;
        }
      }
    }

    if (!body.rows.isEmpty()) {
      for (TableRow row : body.rows) {
        int jSpan = 0;
        int kMax = row.cells.size();
        for (int k = 0; k < kMax; k++) {
          TableCell cell = row.cells.get(k);
          delta.value = 0;
          BasedSequence cellText = cellText(row.cells, k, false, false, 0, null, delta);
          int width =
              options.charWidthProvider.getStringWidth(cellText)
                  + options.spacePad
                  + options.pipeWidth * cell.columnSpan;
          if (cell.columnSpan > 1) {
            columnSpans.add(new ColumnSpan(jSpan, cell.columnSpan, width));
          } else {
            if (columnWidths[jSpan] < width) columnWidths[jSpan] = width;
          }

          jSpan += cell.columnSpan;
        }
      }
    }

    // add separator column widths to the calculation
    if (separator.rows.isEmpty() || !body.rows.isEmpty() || !header.rows.isEmpty()) {
      int j = 0;
      delta.value = 0;
      for (CellAlignment alignment : alignments) {
        CellAlignment alignment1 = adjustCellAlignment(alignment);
        int colonCount =
            alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.RIGHT
                ? 1
                : alignment1 == CellAlignment.CENTER ? 2 : 0;
        int dashCount = 0;
        int dashesOnly =
            Utils.minLimit(
                dashCount,
                options.minSeparatorColumnWidth - colonCount,
                options.minSeparatorDashes);
        if (dashCount < dashesOnly) dashCount = dashesOnly;
        int width =
            dashCount * options.dashWidth + colonCount * options.colonWidth + options.pipeWidth;
        if (columnWidths[j] < width) columnWidths[j] = width;
        j++;
      }
    } else {
      // keep as is
      int j = 0;
      delta.value = 0;
      for (TableCell cell : separator.rows.get(0).cells) {
        CellAlignment alignment = adjustCellAlignment(cell.alignment);
        int colonCount =
            alignment == CellAlignment.LEFT || alignment == CellAlignment.RIGHT
                ? 1
                : alignment == CellAlignment.CENTER ? 2 : 0;
        BasedSequence trim = cell.text.trim(COLON_TRIM_CHARS);
        int dashCount = trim.length();
        int dashesOnly =
            Utils.minLimit(
                dashCount,
                options.minSeparatorColumnWidth - colonCount,
                options.minSeparatorDashes);
        if (dashCount < dashesOnly) dashCount = dashesOnly;
        int width =
            dashCount * options.dashWidth + colonCount * options.colonWidth + options.pipeWidth;
        if (columnWidths[j] < width) columnWidths[j] = width;
        j++;
      }
    }

    if (!columnSpans.isEmpty()) {
      // now need to distribute extra width from spans to contained columns
      BitSet unfixedColumns = new BitSet(sepColumns);
      List newColumnSpans = new ArrayList<>(columnSpans.size());

      for (ColumnSpan columnSpan : columnSpans) {
        int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
        if (spanWidth < columnSpan.width) {
          // not all fits, need to distribute the remainder
          unfixedColumns.set(
              columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan);
          newColumnSpans.add(columnSpan);
        }
      }

      // we now distribute additional width equally between columns that are spanned to unfixed
      // columns
      while (!newColumnSpans.isEmpty()) {
        columnSpans = newColumnSpans;

        BitSet fixedColumns = new BitSet(sepColumns);
        newColumnSpans.clear();

        // remove spans that already fit into fixed columns
        for (ColumnSpan columnSpan : columnSpans) {
          int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
          int fixedWidth =
              spanFixedWidth(unfixedColumns, columnSpan.startColumn, columnSpan.columnSpan);

          if (spanWidth <= fixedWidth) {
            fixedColumns.set(
                columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan);
          } else {
            newColumnSpans.add(columnSpan);
          }
        }

        // reset fixed columns
        unfixedColumns.andNot(fixedColumns);
        columnSpans = newColumnSpans;
        newColumnSpans.clear();

        for (ColumnSpan columnSpan : columnSpans) {
          int spanWidth = spanWidth(columnSpan.startColumn, columnSpan.columnSpan);
          int fixedWidth =
              spanFixedWidth(unfixedColumns, columnSpan.startColumn, columnSpan.columnSpan);

          if (spanWidth > fixedWidth) {
            // not all fits, need to distribute the remainder to unfixed columns
            int distributeWidth = spanWidth - fixedWidth;
            int unfixedColumnCount =
                unfixedColumns
                    .get(columnSpan.startColumn, columnSpan.startColumn + columnSpan.columnSpan)
                    .cardinality();
            int perSpanWidth = distributeWidth / unfixedColumnCount;
            int extraWidth = distributeWidth - perSpanWidth * unfixedColumnCount;

            for (int i = 0; i < columnSpan.columnSpan; i++) {
              if (unfixedColumns.get(columnSpan.startColumn + i)) {
                columnWidths[columnSpan.startColumn + i] += perSpanWidth;
                if (extraWidth > 0) {
                  columnWidths[columnSpan.startColumn + i]++;
                  extraWidth--;
                }
              }
            }
            newColumnSpans.add(columnSpan);
          }
        }
      }
    }
  }

  public void fillMissingColumns() {
    fillMissingColumns(null);
  }

  public void fillMissingColumns(Integer minColumn) {
    int minColumns = getMinColumns();
    int maxColumns = getMaxColumns();
    if (minColumns < maxColumns) {
      // add empty cells to rows that have less
      for (TableRow row : header.rows) {
        row.fillMissingColumns(minColumn, maxColumns);
      }

      for (TableRow row : body.rows) {
        row.fillMissingColumns(minColumn, maxColumns);
      }
    }
  }

  private boolean setTrackedOffsetIndex(int offset, int index) {
    TrackedOffset trackedOffset = findTrackedOffset(offset);
    if (trackedOffset != null) {
      trackedOffset.setIndex(index);
      return true;
    }

    return false;
  }

  /**
   * Transpose table
   *
   * @param columnHeaders number of first columns to use as header rows, 0..maxColumns
   * @return transposed table
   */
  public MarkdownTable transposed(int columnHeaders) {
    MarkdownTable transposed = new MarkdownTable(tableChars, options);
    transposed.trackedOffsets.addAll(trackedOffsets);

    int maxRows = getAllRowsCount() - 1; // don't count separator rows
    int maxCols = getMaxColumns();
    TableCell[][] tableCells = new TableCell[maxRows][];
    for (int i = 0; i < maxRows; i++) {
      tableCells[i] = new TableCell[maxCols];
    }

    // get a matrix of all cells
    forAllSectionsRows(
        0,
        Integer.MAX_VALUE,
        allContentRows,
        (row, allRowsIndex, sectionRows, sectionRowIndex) -> {
          TableCell[] tableCellRow = tableCells[allRowsIndex];
          int iMax = row.cells.size();
          int col = 0;
          for (int i = 0; i < iMax; i++) {
            TableCell cell = row.cells.get(i);
            for (int span = 0; span < cell.columnSpan; span++) {
              tableCellRow[col++] = new TableCell(cell, span == 0, 1, 1, null);
            }
          }
          return 0;
        });

    transposed.setHeader();
    int colHdrs = Math.min(Math.max(0, columnHeaders), maxCols);
    for (int c = 0; c < colHdrs; c++) {
      for (int r = 0; r < maxRows; r++) {
        TableCell cell = tableCells[r][c];
        transposed.addCell(cell == null ? DEFAULT_CELL : cell);
      }
      transposed.nextRow();
    }

    TableRow sepRow = separator.rows.get(0);
    transposed.setSeparator();
    int iMax = sepRow.cells.size();
    for (int i = 0; i < maxRows; i++) {
      if (i < iMax) {
        transposed.addCell(new TableCell(sepRow.cells.get(i), true, 1, 1, null));
      } else {
        transposed.addCell(new TableCell("---", 1, 1));
      }
    }

    transposed.setBody();
    for (int c = colHdrs; c < maxCols; c++) {
      for (int r = 0; r < maxRows; r++) {
        TableCell cell = tableCells[r][c];
        transposed.addCell(cell == null ? DEFAULT_CELL : cell);
      }
      transposed.nextRow();
    }

    transposed.setCaptionCell(getCaptionCell());

    return transposed;
  }

  /**
   * Sort table
   *
   * @param columnSorts column sort information
   * @param textCollectionFlags collection flags to use for collecting cell text
   * @param numericSuffixTester predicate to test non-numeric suffix of numeric column content,
   *     return true if suffix is acceptable, null will result in all suffixes being accepted
   * @return sorted table
   */
  public MarkdownTable sorted(
      ColumnSort[] columnSorts,
      int textCollectionFlags,
      @Nullable NumericSuffixPredicate numericSuffixTester) {
    MarkdownTable sorted = new MarkdownTable(tableChars, options);
    sorted.trackedOffsets.addAll(trackedOffsets);

    sorted.setHeader();
    forAllSectionsRows(
        0,
        Integer.MAX_VALUE,
        allHeaderRows,
        (row, allRowsIndex, sectionRows, sectionRowIndex) -> {
          int iMax = row.cells.size();
          for (int i = 0; i < iMax; i++) {
            TableCell cell = row.cells.get(i);
            sorted.addCell(
                cell == DEFAULT_CELL
                    ? cell
                    : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
          }
          sorted.nextRow();
          return 0;
        });

    sorted.setSeparator();
    TableRow sepRow = separator.rows.get(0);
    int iMax = sepRow.cells.size();
    CellAlignment[] alignments = new CellAlignment[iMax];

    for (int i = 0; i < iMax; i++) {
      TableCell cell = sepRow.cells.get(i);
      sorted.addCell(
          cell == DEFAULT_CELL
              ? cell
              : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
      alignments[i] = cell.alignment;
    }

    List rows = getAllSectionsRows(body);
    int[] cellSizes = new int[iMax];

    int rMax = rows.size();
    int cMax = getMaxBodyColumns();

    for (int r = 0; r < rMax; r++) {
      for (ColumnSort columnSort : columnSorts) {
        int c = columnSort.column;
        if (c >= 0 && c < cMax) {
          IndexSpanOffset spanIndex = rows.get(r).indexOf(c);
          TableCell cell = rows.get(r).cells.get(spanIndex.index);
          if (spanIndex.index == c && cell != null) {
            // not in span
            cellSizes[c] = Math.max(cellSizes[c], cell.text.length());
          }
        }
      }
    }

    TextCollectingVisitor visitor = new TextCollectingVisitor();

    NumericSuffixPredicate numericSuffixPredicate =
        numericSuffixTester == null ? ALL_SUFFIXES_SORT : numericSuffixTester;
    Comparator rowComparator =
        (o1, o2) -> {
          for (ColumnSort columnSort : columnSorts) {
            int c = columnSort.column;
            if (c >= 0 && c < cMax) {
              int cellSize = cellSizes[c];
              if (cellSize > 0) {
                // sorting on this column
                Sort sort = columnSort.sort;
                boolean descending = sort.isDescending();
                boolean numeric = sort.isNumeric();
                boolean numericLast = sort.isNumericLast();

                IndexSpanOffset spanIndex1 = o1.indexOf(c);
                TableCell cell1 = o1.cells.get(spanIndex1.index);

                IndexSpanOffset spanIndex2 = o2.indexOf(c);
                TableCell cell2 = o2.cells.get(spanIndex2.index);
                int result;

                if (spanIndex1.index == c
                    && cell1 != null
                    && spanIndex2.index == c
                    && cell2 != null) {
                  // not in span, compare them by padding and aligning then comparing strings
                  int padLeft1 = 0;
                  int padLeft2 = 0;
                  int padRight1 = 0;
                  int padRight2 = 0;
                  String cellText1 =
                      cell1.tableCellNode == null
                          ? cell1.text.toString()
                          : visitor
                              .collectAndGetText(cell1.tableCellNode, textCollectionFlags)
                              .trim();
                  String cellText2 =
                      cell2.tableCellNode == null
                          ? cell2.text.toString()
                          : visitor
                              .collectAndGetText(cell2.tableCellNode, textCollectionFlags)
                              .trim();
                  int diff1 = cellSize - cellText1.length();
                  int diff2 = cellSize - cellText2.length();

                  switch (alignments[c]) {
                    case CENTER:
                      padLeft1 = diff1 >> 1;
                      padRight1 = cellSize - padLeft1;
                      padLeft2 = diff2 >> 1;
                      padRight2 = cellSize - padLeft2;
                      break;

                    case RIGHT:
                      padLeft1 = diff1;
                      padLeft2 = diff2;
                      break;

                    default:
                      break;
                  }

                  if (numeric) {
                    Pair cellNumeric1 =
                        SequenceUtils.parseNumberPrefixOrNull(cellText1, numericSuffixPredicate);
                    Pair cellNumeric2 =
                        SequenceUtils.parseNumberPrefixOrNull(cellText2, numericSuffixPredicate);

                    if (cellNumeric1 != null && cellNumeric2 != null) {
                      result = compare(cellNumeric1.getFirst(), cellNumeric2.getFirst());
                      String numericSuffix1 = cellNumeric1.getSecond();
                      String numericSuffix2 = cellNumeric2.getSecond();

                      if (result == 0
                          && (numericSuffixPredicate.sortSuffix(numericSuffix1)
                              || numericSuffixPredicate.sortSuffix(numericSuffix2))) {
                        if (!numericSuffix1.isEmpty()
                            && numericSuffixPredicate.sortSuffix(numericSuffix1)
                            && !numericSuffix2.isEmpty()
                            && numericSuffixPredicate.sortSuffix(numericSuffix2)) {
                          result = numericSuffix1.compareTo(cellNumeric2.getSecond());
                        } else if (!numericSuffix1.isEmpty()
                            && numericSuffixPredicate.sortSuffix(numericSuffix1)) {
                          result = numericLast ? -1 : 1;
                          descending = false;
                        } else if (!numericSuffix2.isEmpty()
                            && numericSuffixPredicate.sortSuffix(numericSuffix2)) {
                          result = numericLast ? 1 : -1;
                          descending = false;
                        }
                      }
                    } else if (cellNumeric1 != null) {
                      result = numericLast ? 1 : -1;
                      descending = false; // do not reverse, numbers first/last are not inverted
                    } else if (cellNumeric2 != null) {
                      result = numericLast ? -1 : 1;
                      descending = false; // do not reverse, numbers first/last are not inverted
                    } else {
                      // compare as text
                      String cell1Text =
                          RepeatedSequence.ofSpaces(padLeft1).toString()
                              + cellText1
                              + RepeatedSequence.ofSpaces(padRight1);
                      String cell2Text =
                          RepeatedSequence.ofSpaces(padLeft2).toString()
                              + cellText2
                              + RepeatedSequence.ofSpaces(padRight2);
                      result = cell1Text.compareTo(cell2Text);
                    }
                  } else {
                    String cell1Text =
                        RepeatedSequence.ofSpaces(padLeft1).toString()
                            + cellText1
                            + RepeatedSequence.ofSpaces(padRight1);
                    String cell2Text =
                        RepeatedSequence.ofSpaces(padLeft2).toString()
                            + cellText2
                            + RepeatedSequence.ofSpaces(padRight2);
                    result = cell1Text.compareTo(cell2Text);
                  }
                } else if (spanIndex1.index == c && cell1 != null) {
                  // second in spanned column, so it is first
                  result = 1;
                } else if (spanIndex2.index == c && cell2 != null) {
                  // first in spanned column, so it is first
                  result = -1;
                } else {
                  result = 0;
                }

                if (result != 0) {
                  return descending ? -result : result;
                }
              }
            }
          }
          return 0;
        };

    rows.sort(rowComparator);

    sorted.setBody();
    rMax = rows.size();
    for (int r = 0; r < rMax; r++) {
      TableRow row = rows.get(r);
      iMax = row.cells.size();
      for (int i = 0; i < iMax; i++) {
        TableCell cell = row.cells.get(i);
        sorted.addCell(
            cell == DEFAULT_CELL
                ? cell
                : new TableCell(cell, true, cell.rowSpan, cell.columnSpan, cell.alignment));
      }
      sorted.nextRow();
    }

    sorted.setCaptionCell(getCaptionCell());
    return sorted;
  }

  int appendDashes(LineAppendable out, int dashCount, BasedSequence sepDashes, int dashOffset) {
    int sepDashesLength = sepDashes.length();
    int remainingDashes = Math.max(0, sepDashesLength - dashOffset);

    if (remainingDashes >= dashCount) {
      out.append(sepDashes.subSequence(dashOffset, dashOffset + dashCount));
      remainingDashes -= dashCount;
    } else {
      int usedUpDashes = 0;
      if (remainingDashes > 1) {
        out.append(sepDashes.subSequence(dashOffset, dashOffset + 1));
        remainingDashes--;
        usedUpDashes++;
      }

      out.append('-', dashCount - Math.max(0, remainingDashes + usedUpDashes));

      if (remainingDashes > 0) {
        out.append(sepDashes.subSequence(dashOffset, dashOffset + remainingDashes));
        remainingDashes = 0;
      }
    }
    return sepDashesLength - remainingDashes;
  }

  public void appendTable(LineAppendable out) {
    // we will prepare the separator based on max columns
    CharSequence linePrefix = formatTableIndentPrefix;

    trackedOffsets.sort(Comparator.comparing(TrackedOffset::getOffset));

    out.pushOptions();
    out.removeOptions(LineAppendable.F_WHITESPACE_REMOVAL);

    finalizeTable();

    appendRows(out, header.rows, true, linePrefix);

    {
      out.append(linePrefix);

      TableRow row = !separator.rows.isEmpty() ? separator.rows.get(0) : null;

      if (row != null && row.beforeOffset != NOT_TRACKED) {
        setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
      }

      int j = 0;
      Ref delta = new Ref<>(0);
      for (CellAlignment alignment : alignments) {
        CellAlignment alignment1 = adjustCellAlignment(alignment);

        int colonCount =
            alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.RIGHT
                ? 1
                : alignment1 == CellAlignment.CENTER ? 2 : 0;
        int diff = columnWidths[j] - colonCount * options.colonWidth - options.pipeWidth;
        int dashCount = (delta.value + diff) / options.dashWidth;
        int dashesOnly =
            Utils.minLimit(
                dashCount,
                options.minSeparatorColumnWidth - colonCount,
                options.minSeparatorDashes);
        if (dashCount < dashesOnly) dashCount = dashesOnly;

        if (Math.abs(delta.value + diff - (dashCount + 1) * options.dashWidth)
            < Math.abs(delta.value + diff - dashCount * options.dashWidth)) {
          dashCount++;
        }

        delta.value += diff - dashCount * options.dashWidth;

        int trackedPos;
        TableCell cell = null;
        TableCell previousCell = null;

        if (row != null) {
          List cells = row.cells;
          if (j < cells.size()) {
            cell = cells.get(j);
            if (j > 0) previousCell = cells.get(j - 1);
          }
        }

        trackedPos = cell == null ? NOT_TRACKED : minLimit(cell.trackedTextOffset, 0);
        BasedSequence sepText =
            cell == null ? BasedSequence.NULL : cell.text.trim(COLON_TRIM_CHARS);
        int sepDashOffset = 0;

        if (trackedPos != NOT_TRACKED) {
          if (options.leadTrailPipes && j == 0) out.append('|');
          boolean beforeFirstColon = trackedPos == 0 && cell.text.charAt(trackedPos) == ':';
          boolean afterFirstColon = trackedPos == 1 && cell.text.charAt(trackedPos - 1) == ':';
          boolean beforeLastColon =
              trackedPos == cell.text.length() - 1 && cell.text.charAt(trackedPos) == ':';
          boolean afterLastColon =
              trackedPos == cell.text.length() && cell.text.charAt(trackedPos - 1) == ':';
          boolean afterLastDash =
              trackedPos == cell.text.length() && cell.text.charAt(trackedPos - 1) == '-';

          if (alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.CENTER) {
            if (beforeFirstColon) {
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
              out.append(':');
            } else if (afterFirstColon) {
              out.append(':');
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
            } else {
              out.append(':');
            }
          } else {
            beforeFirstColon = false;
            afterFirstColon = false;
          }

          if (!afterFirstColon && !beforeFirstColon && !afterLastColon && !beforeLastColon) {
            if (trackedPos == 0) {
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
              sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
            } else if (!afterLastDash && trackedPos < dashCount) {
              sepDashOffset = appendDashes(out, trackedPos, sepText, sepDashOffset);
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              sepDashOffset = appendDashes(out, dashCount - trackedPos, sepText, sepDashOffset);
              trackedPos = NOT_TRACKED;
            } else {
              sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
            }
          } else {
            sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);
          }

          if (alignment1 == CellAlignment.RIGHT || alignment1 == CellAlignment.CENTER) {
            if (afterLastColon) {
              out.append(':');
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
            } else if (beforeLastColon) {
              setTrackedOffsetIndex(
                  cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                  out.offsetWithPending());
              trackedPos = NOT_TRACKED;
              out.append(':');
            } else {
              out.append(':');
            }
          } else if (afterLastColon || beforeLastColon) {
            setTrackedOffsetIndex(
                cell.trackedTextOffset + cell.getInsideStartOffset(previousCell),
                out.offsetWithPending());
            trackedPos = NOT_TRACKED;
          }
        } else {
          if (options.leadTrailPipes && j == 0) out.append('|');
          if (alignment1 == CellAlignment.LEFT || alignment1 == CellAlignment.CENTER) {
            out.append(':');
          }

          sepDashOffset = appendDashes(out, dashCount, sepText, sepDashOffset);

          if (alignment1 == CellAlignment.RIGHT || alignment1 == CellAlignment.CENTER) {
            out.append(':');
          }
        }

        j++;
        if (options.leadTrailPipes || j < alignments.length) out.append('|');
      }

      if (row != null && row.afterOffset != NOT_TRACKED) {
        setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
      }

      out.line();
    }

    appendRows(out, body.rows, false, linePrefix);

    TableCell captionCell = getCaptionCell();
    String captionText = formattedCaption(captionCell.text, options);
    if (captionText != null) {
      BasedSequence formattedCaption =
          BasedSequence.of(captionText).subSequence(0, captionText.length());
      boolean handled = false;

      if (!this.caption.rows.isEmpty()) {
        TableRow row = this.caption.rows.get(0);
        if (captionCell.trackedTextOffset != NOT_TRACKED
            || row.beforeOffset != NOT_TRACKED
            || row.afterOffset != NOT_TRACKED) {
          TableCell cell = captionCell;

          out.line();

          if (row.beforeOffset != NOT_TRACKED) {
            setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
          }

          captionCell = captionCell.withText(captionCell.text.trim());
          if (cell.trackedTextOffset != NOT_TRACKED) {
            captionCell =
                captionCell.withTrackedOffset(
                    minLimit(cell.trackedTextOffset - cell.text.trimmedStart().length(), 0));
          }

          boolean addOpenCaptionSpace = false;
          boolean addCloseCaptionSpace = false;

          if (!captionCell.text.isBlank()) {
            switch (options.formatTableCaptionSpaces) {
              case ADD:
                addOpenCaptionSpace = true;
                addCloseCaptionSpace = true;
                break;

              case REMOVE:
                break;

              default:
              case AS_IS:
                addOpenCaptionSpace = cell.text.startsWith(" ");
                addCloseCaptionSpace = cell.text.endsWith(" ");
                break;
            }
          }

          out.append(linePrefix);
          out.append('[');
          if (addOpenCaptionSpace) out.append(' ');

          int cellOffset = out.offsetWithPending();

          row.cells.set(0, captionCell);
          Ref delta = new Ref<>(0);
          BasedSequence cellText =
              cellText(row.cells, 0, true, false, 0, CellAlignment.LEFT, delta);
          out.offsetWithPending();

          if (!row.cells.isEmpty()) {
            if (cell.trackedTextOffset != NOT_TRACKED) {
              TableCell adjustedCell = row.cells.get(0);
              if (adjustedCell.trackedTextOffset != NOT_TRACKED) {
                setTrackedOffsetIndex(
                    cell.trackedTextOffset + cell.text.getStartOffset(),
                    cellOffset
                        + (cellText.isBlank()
                            ? 1
                            : minLimit(adjustedCell.trackedTextOffset, 0)
                                + adjustedCell.trackedTextAdjust));
              }
            }
            row.cells.set(0, cell);
          } else {
            row.cells.add(cell);
          }

          out.append(cellText);

          if (addCloseCaptionSpace) out.append(' ');
          out.append(']');

          if (row.afterOffset != NOT_TRACKED) {
            setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
          }
          out.line();

          handled = true;
        }
      }

      if (!handled) {
        out.popOptions().pushOptions();
        out.line().append(linePrefix).append('[').append(formattedCaption).append(']').line();
      }
    }
    out.popOptions();
  }

  public static void appendFormattedCaption(
      LineAppendable out, BasedSequence caption, TableFormatOptions options) {
    String formattedCaption = formattedCaption(caption, options);
    if (formattedCaption != null) {
      out.line().append('[').append(formattedCaption).append(']').line();
    }
  }

  public static String formattedCaption(BasedSequence caption, TableFormatOptions options) {
    boolean append = caption.isNotNull();

    switch (options.formatTableCaption) {
      case ADD:
        append = true;
        break;

      case REMOVE_EMPTY:
        append = !(caption.isBlank());
        break;

      case REMOVE:
        append = false;
        break;

      default:
      case AS_IS:
        break;
    }

    if (append) {
      String captionSpaces = "";

      switch (options.formatTableCaptionSpaces) {
        case ADD:
          captionSpaces = " ";
          break;

        case REMOVE:
          break;

        default:
        case AS_IS:
          break;
      }
      return captionSpaces + caption.toString() + captionSpaces;
    }
    return null;
  }

  private static boolean pipeNeedsSpaceBefore(TableCell cell) {
    return cell.text.equals(" ") || !cell.text.endsWith(" ");
  }

  private static boolean pipeNeedsSpaceAfter(TableCell cell) {
    return cell.text.equals(" ") || !cell.text.startsWith(" ");
  }

  private void appendRows(
      LineAppendable out, List rows, boolean isHeader, CharSequence linePrefix) {
    for (TableRow row : rows) {
      int j = 0;
      int jSpan = 0;
      Ref delta = new Ref<>(0);

      out.append(linePrefix);

      if (row.beforeOffset != NOT_TRACKED) {
        setTrackedOffsetIndex(row.beforeOffset, out.offsetWithPending());
      }

      int iMax = row.cells.size();
      for (int i = 0; i < iMax; i++) {
        TableCell cell = row.cells.get(i);

        if (j == 0) {
          if (options.leadTrailPipes) {
            out.append('|');
            if (options.spaceAroundPipes && pipeNeedsSpaceAfter(cell)) {
              out.append(' ');
            }
          }
        } else {
          if (options.spaceAroundPipes && pipeNeedsSpaceAfter(cell)) out.append(' ');
        }

        CellAlignment cellAlignment =
            isHeader && cell.alignment != CellAlignment.NONE ? cell.alignment : alignments[jSpan];

        BasedSequence cellText =
            cellText(
                row.cells,
                i,
                true,
                isHeader,
                spanWidth(jSpan, cell.columnSpan)
                    - options.spacePad
                    - options.pipeWidth * cell.columnSpan,
                cellAlignment,
                delta);

        if (cell.trackedTextOffset != NOT_TRACKED) {
          TableCell adjustedCell = row.cells.get(i);
          if (adjustedCell.trackedTextOffset != NOT_TRACKED) {
            int cellOffset = out.offsetWithPending();
            int adjustForBlank = cell.text.isBlank() ? -1 : 0;
            if (!setTrackedOffsetIndex(
                cell.trackedTextOffset
                    + cell.getTextStartOffset(i == 0 ? null : row.cells.get(i - 1)),
                cellOffset
                    + minLimit(adjustedCell.trackedTextOffset + adjustForBlank, 0)
                    + adjustedCell.trackedTextAdjust)) {
              // QUERY: Triggered after sort table in MdNav for header row
            }
          }
        }

        out.append(cellText);

        j++;
        jSpan += cell.columnSpan;

        if (j < alignments.length) {
          if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
          appendColumnSpan(out, cell.columnSpan, cell.getInsideEndOffset(), cell.spanTrackedOffset);
        } else if (options.leadTrailPipes) {
          if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
          appendColumnSpan(out, cell.columnSpan, cell.getInsideEndOffset(), cell.spanTrackedOffset);
        } else {
          if (options.spaceAroundPipes && pipeNeedsSpaceBefore(cell)) out.append(' ');
          appendColumnSpan(
              out, cell.columnSpan - 1, cell.getInsideEndOffset(), cell.spanTrackedOffset);
        }
      }

      if (row.afterOffset != NOT_TRACKED) {
        setTrackedOffsetIndex(row.afterOffset, out.offsetWithPending());
      }

      if (j > 0) out.line();
    }
  }

  private void appendColumnSpan(
      LineAppendable out, int span, int cellInsideEndOffset, int trackedSpanOffset) {
    if (trackedSpanOffset == NOT_TRACKED) {
      out.append('|', span);
    } else {
      if (trackedSpanOffset == 0) {
        setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
        out.append('|', span);
      } else if (trackedSpanOffset < span) {
        out.append('|', trackedSpanOffset);
        setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
        out.append('|', span - trackedSpanOffset);
      } else {
        out.append('|', span);
        setTrackedOffsetIndex(cellInsideEndOffset + trackedSpanOffset, out.offsetWithPending());
      }
    }
  }

  private BasedSequence cellText(
      List cells,
      int index,
      boolean withTrackedOffset,
      boolean isHeader,
      int width,
      CellAlignment alignment,
      Ref delta) {
    TableCell cell = cells.get(index);
    TableCell adjustedCell = cell;
    BasedSequence text = cell.text;
    boolean needsPadding =
        cell.trackedTextOffset != NOT_TRACKED && cell.trackedTextOffset >= cell.text.length();
    boolean neededPrefix = false;

    if (cell.trackedTextOffset != NOT_TRACKED) {
      if (cell.trackedTextOffset > cell.text.length()) {
        // add padding spaces
        int suffixed = cell.trackedTextOffset - cell.text.length() - 1;
        text = text.append(RepeatedSequence.repeatOf(' ', suffixed));
      } else if (cell.trackedTextOffset < 0) {
        neededPrefix = true;
      }
    }

    int length = options.charWidthProvider.getStringWidth(text);
    if (options.adjustColumnWidth
        && (length < width || cell.trackedTextOffset > cell.text.length())) {
      if (!options.applyColumnAlignment || alignment == null || alignment == CellAlignment.NONE) {
        alignment =
            isHeader && options.leftAlignMarker != ADD ? CellAlignment.CENTER : CellAlignment.LEFT;
      } else if (isHeader
          && alignment == CellAlignment.LEFT
          && options.leftAlignMarker == DiscretionaryText.REMOVE) {
        alignment = CellAlignment.CENTER;
      }

      int diff = width - length;
      int spaceCount = (delta.value + diff) / options.spaceWidth;
      // NOTE: add extra space if accumulated adding extra space gives smaller abs error
      if (width > 0
          && Math.abs(delta.value + diff - (spaceCount + 1) * options.spaceWidth)
              < Math.abs(delta.value + diff - spaceCount * options.spaceWidth)) {
        spaceCount++;
      }

      delta.value += diff - spaceCount * options.spaceWidth;

      switch (alignment) {
        case LEFT:
          if (spaceCount > 0) {
            text =
                text.append(PrefixedSubSequence.repeatOf(" ", spaceCount, text.getEmptySuffix()));
          }

          if (withTrackedOffset && needsPadding && cell.afterSpace) {
            // if did not grow then move caret right
            if (spaceCount <= 0) {
              adjustedCell = adjustedCell.withTrackedTextAdjust(1);
            }
          }
          break;

        case RIGHT:
          if (spaceCount > 0) {
            text = PrefixedSubSequence.repeatOf(" ", spaceCount, text);

            if (withTrackedOffset && cell.trackedTextOffset != NOT_TRACKED) {
              adjustedCell =
                  cell.withTrackedOffset(
                      maxLimit(text.length(), cell.trackedTextOffset + spaceCount));
            }

            if (withTrackedOffset && neededPrefix && cell.afterSpace) {
              adjustedCell = adjustedCell.withTrackedTextAdjust(1);
            }
          }

          if (withTrackedOffset && needsPadding && cell.afterSpace) {
            if (spaceCount <= 0 || !cell.afterDelete) {
              adjustedCell = adjustedCell.withTrackedTextAdjust(1);
            }
          }
          break;

        case CENTER:
          int count = spaceCount / 2;
          if (spaceCount > 0) {
            text =
                PrefixedSubSequence.repeatOf(" ", count, text)
                    .append(
                        PrefixedSubSequence.repeatOf(
                            " ", spaceCount - count, text.getEmptySuffix()));

            if (withTrackedOffset && cell.trackedTextOffset != NOT_TRACKED) {
              adjustedCell =
                  cell.withTrackedOffset(maxLimit(text.length(), cell.trackedTextOffset + count));
            }

            if (withTrackedOffset && neededPrefix && cell.afterSpace) {
              adjustedCell = adjustedCell.withTrackedTextAdjust(1);
            }
          } else {
            if (withTrackedOffset && needsPadding && cell.afterSpace) {
              adjustedCell = adjustedCell.withTrackedTextAdjust(1);
            }
          }
          break;
      }
    }

    if (withTrackedOffset && adjustedCell.trackedTextOffset != NOT_TRACKED) {
      // replace with adjusted offset
      if (adjustedCell.trackedTextOffset > text.length()) {
        adjustedCell = adjustedCell.withTrackedOffset(text.length());
      }

      if (adjustedCell != cell) cells.set(index, adjustedCell);
    }

    return text;
  }

  private int spanWidth(int col, int columnSpan) {
    if (columnSpan > 1) {
      int width = 0;
      for (int i = 0; i < columnSpan; i++) {
        width += columnWidths[i + col];
      }
      return width;
    }

    return columnWidths[col];
  }

  private int spanFixedWidth(BitSet unfixedColumns, int col, int columnSpan) {
    if (columnSpan > 1) {
      int width = 0;
      for (int i = 0; i < columnSpan; i++) {
        if (!unfixedColumns.get(i)) {
          width += columnWidths[i + col];
        }
      }
      return width;
    }

    return unfixedColumns.get(col) ? 0 : columnWidths[col];
  }

  private static class ColumnSpan {
    final int startColumn;
    final int columnSpan;
    final int width;
    int additionalWidth;

    public ColumnSpan(int startColumn, int columnSpan, int width) {
      this.startColumn = startColumn;
      this.columnSpan = columnSpan;
      this.width = width;
      this.additionalWidth = 0;
    }
  }

  private CellAlignment adjustCellAlignment(CellAlignment alignment) {
    switch (options.leftAlignMarker) {
      case ADD:
        if (alignment == null || alignment == CellAlignment.NONE) {
          alignment = CellAlignment.LEFT;
        }
        break;
      case REMOVE:
        if (alignment == CellAlignment.LEFT) alignment = CellAlignment.NONE;
        break;

      default:
        break;
    }
    return alignment;
  }

  private static int aggregateTotalColumnsWithoutColumns(
      TableSection[] sections, BinaryOperator aggregator, int... skipColumns) {
    Integer[] columns = new Integer[] {null};

    forAllSectionsRows(
        0,
        Integer.MAX_VALUE,
        sections,
        (row, allRowsIndex, rows, index) -> {
          int iMax = row.cells.size();
          int count = 0;
          for (int i = 0; i < iMax; i++) {
            if (!ArrayUtils.contained(i, skipColumns)) count += row.cells.get(i).columnSpan;
          }
          if (count != 0) {
            columns[0] = aggregator.apply(columns[0], count);
          }
          return 0;
        });

    return columns[0] == null ? 0 : columns[0];
  }

  private static int aggregateTotalColumnsWithoutRows(
      TableSection[] sections, BinaryOperator aggregator, int... skipRows) {
    Integer[] columns = new Integer[] {null};

    forAllSectionsRows(
        0,
        Integer.MAX_VALUE,
        sections,
        (row, allRowsIndex, rows, index) -> {
          if (!ArrayUtils.contained(allRowsIndex, skipRows)) {
            int totalColumns = row.getTotalColumns();
            if (totalColumns > 0) {
              columns[0] = aggregator.apply(columns[0], totalColumns);
            }
          }
          return 0;
        });

    return columns[0] == null ? 0 : columns[0];
  }

  private static void forAllSectionsRows(
      int startIndex, int count, TableSection[] sections, TableRowManipulator manipulator) {
    if (count <= 0) {
      return;
    }
    int remaining = count;
    int sectionIndex = startIndex;
    int allRowsIndex = startIndex;

    for (TableSection section : sections) {
      int currentIndex;

      if (sectionIndex >= section.rows.size()) {
        sectionIndex -= section.rows.size();
        continue;
      }

      currentIndex = sectionIndex;
      sectionIndex = 0;

      while (currentIndex < section.rows.size()) {
        int result =
            manipulator.apply(
                section.rows.get(currentIndex), allRowsIndex, section.rows, currentIndex);
        if (result == TableRowManipulator.BREAK) {
          return;
        }
        if (result < 0) {
          allRowsIndex -= result; // adjust for deleted rows
          remaining += result;
        } else {
          currentIndex += result + 1;
          remaining--;
        }
        if (remaining <= 0) {
          return;
        }
        allRowsIndex++;
      }
    }
  }

  public static class IndexSpanOffset {
    public final int index;
    public final int spanOffset;

    public IndexSpanOffset(int index, int spanOffset) {
      this.index = index;
      this.spanOffset = spanOffset;
    }

    @Override
    public String toString() {
      return "IndexSpanOffset{" + "index=" + index + ", spanOffset=" + spanOffset + '}';
    }
  }

  @Override
  public String toString() {
    // NOTE: show not simple name but name of container class if any
    return this.getClass().getName().substring(getClass().getPackage().getName().length() + 1)
        + "{"
        + "header="
        + header
        + ",\nseparator="
        + separator
        + ",\nbody="
        + body
        + ",\ncaption="
        + caption
        + ",\noptions="
        + options
        + ",\ntrackedOffsets="
        + trackedOffsets
        + "}";
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy