com.vladsch.flexmark.util.format.MarkdownTable Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of flexmark-util-format Show documentation
Show all versions of flexmark-util-format Show documentation
flexmark-java format utility classes
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