Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for table cells that contain multiple lines #11

Open
rafaelzita opened this issue May 4, 2019 · 2 comments
Open

Support for table cells that contain multiple lines #11

rafaelzita opened this issue May 4, 2019 · 2 comments

Comments

@rafaelzita
Copy link

Hello I was trying to create a table with a cell that contains text spanning multiple lines.
It seems that output can't handle it and looks broken.

Table.Builder tableBuilder = new Table.Builder()
.withAlignments(Table.ALIGN_LEFT, Table.ALIGN_LEFT, Table.ALIGN_LEFT)
.addRow(new BoldText("Version"), new BoldText("Comment"))
.addRow("1.0", "Initial Version\nChanged by Joe")
.addRow("2.0", "");

System.out.println(tableBuilder.build());

What happens now

Version Comment
1.0 Initial Version
Changed by Joe
2.0 New Changes

Expected:

Version Comment
1.0 Initial Version
Changed by Joe
2.0 New Changes
@rafaelzita
Copy link
Author

I managed to fix this by modifying Table.java


package markdowntest.markdowntest;

import static net.steppschuh.markdowngenerator.util.StringUtil.surroundValueWith;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.steppschuh.markdowngenerator.MarkdownElement;
import net.steppschuh.markdowngenerator.table.TableRow;
import net.steppschuh.markdowngenerator.util.StringUtil;

public class Table extends MarkdownElement {

public static final String NEWLINE_REGEX = "\r?\n";
private static final String EQUALS = "=";
public static final String DASH = "-";

public static final String COLUMN_SEPERATOR = "|";
public static final String TABLE_EDGE_SEPERATOR = "+";
public static final String WHITESPACE = " ";
public static final String DEFAULT_TRIMMING_INDICATOR = "~";
public static final int DEFAULT_MINIMUM_COLUMN_WIDTH = 3;

public static final int ALIGN_CENTER = 1;
public static final int ALIGN_LEFT = 2;
public static final int ALIGN_RIGHT = 3;

private List<TableRow> rows;
private List<Integer> alignments;
private boolean firstRowIsHeader = true;
private int minimumColumnWidth = DEFAULT_MINIMUM_COLUMN_WIDTH;
private String trimmingIndicator = DEFAULT_TRIMMING_INDICATOR;

public static class Builder {

    private Table table;
    private int rowLimit;

    public Builder() {
        table = new Table();
    }

    public Builder withRows(List<TableRow> tableRows) {
        table.setRows(tableRows);
        return this;
    }

    public Builder addRow(TableRow tableRow) {
        table.getRows().add(tableRow);
        return this;
    }

    public Builder addRow(Object... objects) {
        TableRow tableRow = new TableRow(Arrays.asList(objects));
        table.getRows().add(tableRow);
        return this;
    }

    public Builder withAlignments(List<Integer> alignments) {
        table.setAlignments(alignments);
        return this;
    }

    public Builder withAlignments(Integer... alignments) {
        return withAlignments(Arrays.asList(alignments));
    }

    public Builder withAlignment(int alignment) {
        return withAlignments(Collections.singletonList(alignment));
    }

    public Builder withRowLimit(int rowLimit) {
        this.rowLimit = rowLimit;
        return this;
    }

    public Builder withTrimmingIndicator(String trimmingIndicator) {
        table.setTrimmingIndicator(trimmingIndicator);
        return this;
    }

    public Table build() {
        if (rowLimit > 0) {
            table.trim(rowLimit);
        }
        return table;
    }

}

public Table() {
    this.rows = new ArrayList<>();
    this.alignments = new ArrayList<>();
    firstRowIsHeader = true;
}

public Table(List<TableRow> rows) {
    this();
    this.rows = rows;
}

public Table(List<TableRow> rows, List<Integer> alignments) {
    this(rows);
    this.alignments = alignments;
}

@Override
public String serialize() {
    Map<Integer, Integer> columnWidths = getColumnWidths(rows, minimumColumnWidth);

    StringBuilder sb = new StringBuilder();

    String headerSeperator = generateHeaderSeperator(columnWidths, alignments);
    String rowSeperator = generateRowSeperator(columnWidths);
    boolean headerSeperatorAdded = !firstRowIsHeader;
    if (!firstRowIsHeader) {
        sb.append(headerSeperator).append(System.lineSeparator());
    }

    for (TableRow row : rows) {
    	// Splits the columns value to multiple lines and collects them
    	Map<Integer, List<String>> valueLinesOfColumns = new HashMap<Integer, List<String>>();
    	int maxColumnValueLinesSize = 1;
        for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) {
			
        	String rawColumnValue = (row.getColumns().get(columnIndex) != null)? row.getColumns().get(columnIndex).toString(): "";
        	// Duplicates the new line (to indicate in markup that this is really a new line 
        	// and not a continuation)
        	List<String> valueLinesOfSingleColumn = Arrays.asList(rawColumnValue.replaceAll("(r?\n)", "$1$1").split(NEWLINE_REGEX));
        	valueLinesOfColumns.put(columnIndex, valueLinesOfSingleColumn);

        	// get the maximum number of lines for all the columns, this will
        	// will determine the number of lines for the whole row
        	maxColumnValueLinesSize = Math.max(maxColumnValueLinesSize, valueLinesOfSingleColumn.size());

    	}
    	
		if (!headerSeperatorAdded) {
			sb.append(rowSeperator).append(System.lineSeparator());
		}

        for (int columnValueLineIndex = 0; columnValueLineIndex < maxColumnValueLinesSize; columnValueLineIndex++) {

			for (int columnIndex = 0; columnIndex < columnWidths.size(); columnIndex++) {
				sb.append(COLUMN_SEPERATOR);

				String value = "";
				if (row.getColumns().size() > columnIndex) {
					//Object valueObject = row.getColumns().get(columnIndex);
					//if (valueObject != null) {
					//	value = valueObject.toString();
					//}
					if (valueLinesOfColumns.get(columnIndex).size() > columnValueLineIndex) {
						value = valueLinesOfColumns.get(columnIndex).get(columnValueLineIndex);
					}
				}

				if (value.equals(trimmingIndicator)) {
					value = StringUtil.fillUpLeftAligned(value, trimmingIndicator, columnWidths.get(columnIndex));
					value = surroundValueWith(value, WHITESPACE);
				} else {
					int alignment = getAlignment(alignments, columnIndex);
					value = surroundValueWith(value, WHITESPACE);
					value = StringUtil.fillUpAligned(value, WHITESPACE, columnWidths.get(columnIndex) + 2, alignment);
				}

				sb.append(value);

				if (columnIndex == row.getColumns().size() - 1) {
					sb.append(COLUMN_SEPERATOR);
				}

			}

// if (rows.indexOf(row) < rows.size() - 1) {
sb.append(System.lineSeparator());
// }

        }
		if (!headerSeperatorAdded) {
			sb.append(headerSeperator).append(System.lineSeparator());
			headerSeperatorAdded = true;
		} else {
			sb.append(rowSeperator).append(System.lineSeparator());
        }

    }
    return sb.toString();
}

/**
 * Removes {@link TableRow}s from the center of this table until only the requested amount of
 * rows is left.
 *
 * @param rowsToKeep Amount of {@link TableRow}s that should not be removed
 * @return the trimmed table
 */
public Table trim(int rowsToKeep) {
    rows = trim(this, rowsToKeep, trimmingIndicator).getRows();
    return this;
}

/**
 * Removes {@link TableRow}s from the center of the specified table until only the requested
 * amount of rows is left.
 *
 * @param table      Table to remove {@link TableRow}s from
 * @param rowsToKeep Amount of {@link TableRow}s that should not be removed
 * @param trimmingIndicator The content that trimmed cells should be filled with
 * @return The trimmed table
 */
public static Table trim(Table table, int rowsToKeep, String trimmingIndicator) {
    if (table.getRows().size() <= rowsToKeep) {
        return table;
    }

    int trimmedEntriesCount = table.getRows().size() - (table.getRows().size() - rowsToKeep);
    int trimmingStartIndex = Math.round(trimmedEntriesCount / 2) + 1;
    int trimmingStopIndex = table.getRows().size() - trimmingStartIndex;

    List<TableRow> trimmedRows = new ArrayList<>();
    for (int i = trimmingStartIndex; i <= trimmingStopIndex; i++) {
        trimmedRows.add(table.getRows().get(i));
    }

    table.getRows().removeAll(trimmedRows);

    TableRow trimmingIndicatorRow = new TableRow();
    for (int columnIndex = 0; columnIndex < table.getRows().get(0).getColumns().size(); columnIndex++) {
        trimmingIndicatorRow.getColumns().add(trimmingIndicator);
    }
    table.getRows().add(trimmingStartIndex, trimmingIndicatorRow);

    return table;
}

public static String generateHeaderSeperator(Map<Integer, Integer> columnWidths, List<Integer> alignments) {
    StringBuilder sb = new StringBuilder();
    for (int columnIndex = 0; columnIndex < columnWidths.entrySet().size(); columnIndex++) {
        sb.append(TABLE_EDGE_SEPERATOR);

        // String value = StringUtil.fillUpLeftAligned("", "-", columnWidths.get(columnIndex));
        String value = StringUtil.fillUpLeftAligned("", EQUALS, columnWidths.get(columnIndex));

        int alignment = getAlignment(alignments, columnIndex);
        switch (alignment) {
            case ALIGN_RIGHT: {
                value = DASH + value + ":";
                break;
            }
            case ALIGN_CENTER: {
                value = ":" + value + ":";
                break;
            }
            default: {
                value = surroundValueWith(value, EQUALS);
                break;
            }
        }

        sb.append(value);
        if (columnIndex == columnWidths.entrySet().size() - 1) {
            sb.append(TABLE_EDGE_SEPERATOR);
        }
    }
    return sb.toString();
}

public static String generateRowSeperator(Map<Integer, Integer> columnWidths) {
    StringBuilder sb = new StringBuilder();
    for (int columnIndex = 0; columnIndex < columnWidths.entrySet().size(); columnIndex++) {
        sb.append(TABLE_EDGE_SEPERATOR);
        // String value = StringUtil.fillUpLeftAligned("", "-", columnWidths.get(columnIndex));
        String value = StringUtil.fillUpLeftAligned("", DASH, columnWidths.get(columnIndex));
		value = surroundValueWith(value, DASH);
        sb.append(value);
        if (columnIndex == columnWidths.entrySet().size() - 1) {
            sb.append(TABLE_EDGE_SEPERATOR);
        }
    }
    return sb.toString();
}


public static Map<Integer, Integer> getColumnWidths(List<TableRow> rows, int minimumColumnWidth) {
    Map<Integer, Integer> columnWidths = new HashMap<Integer, Integer>();
    if (rows.isEmpty()) {
        return columnWidths;
    }
    for (int columnIndex = 0; columnIndex < rows.get(0).getColumns().size(); columnIndex++) {
        columnWidths.put(columnIndex, getMaximumItemLength(rows, columnIndex, minimumColumnWidth));
    }
    return columnWidths;
}

public static int getMaximumItemLength(List<TableRow> rows, int columnIndex, int minimumColumnWidth) {
    int maximum = minimumColumnWidth;
    for (TableRow row : rows) {
        if (row.getColumns().size() < columnIndex + 1) {
            continue;
        }
        Object value = row.getColumns().get(columnIndex);
        if (value == null) {
            continue;
        }
        for (String valueLine: value.toString().split(NEWLINE_REGEX)) {
			maximum = Math.max(valueLine.toString().length(), maximum);
        }
    }
    return maximum;
}

public static int getAlignment(List<Integer> alignments, int columnIndex) {
    if (alignments.isEmpty()) {
        return ALIGN_LEFT;
    }
    if (columnIndex >= alignments.size()) {
        columnIndex = alignments.size() - 1;
    }
    return alignments.get(columnIndex);
}

public List<TableRow> getRows() {
    return rows;
}

public void setRows(List<TableRow> rows) {
    this.rows = rows;
    invalidateSerialized();
}

public List<Integer> getAlignments() {
    return alignments;
}

public void setAlignments(List<Integer> alignments) {
    this.alignments = alignments;
    invalidateSerialized();
}

public boolean isFirstRowHeader() {
    return firstRowIsHeader;
}

public void useFirstRowAsHeader(boolean firstRowIsHeader) {
    this.firstRowIsHeader = firstRowIsHeader;
    invalidateSerialized();
}

public int getMinimumColumnWidth() {
    return minimumColumnWidth;
}

public void setMinimumColumnWidth(int minimumColumnWidth) {
    this.minimumColumnWidth = minimumColumnWidth;
    invalidateSerialized();
}

public String getTrimmingIndicator() {
    return trimmingIndicator;
}

public void setTrimmingIndicator(String trimmingIndicator) {
    this.trimmingIndicator = trimmingIndicator;
}

}

Note that I changed the format of the markdown table to be compatible with pandoc's markdown.
Ex.
+-------------+-----------------+
| Version | Comment |
+=============+=================+
| 1.0 | Initial Version |
| | |
| | Changed by Joe |
+-------------+-----------------+
| 2.0 | New Changes |
+-------------+-----------------+

@Steppschuh
Copy link
Owner

Hey Rafael,
thanks for your input. This library tries to support most Markdown Flavors, but especially the commonly used GitHub Flavored Markdown. According to the specs, merged columns or cells are not supported. You can see that by actually using your posted examples:

Version Comment
1.0 Initial Version
Changed by Joe
2.0 New Changes
Version Comment
1.0 Initial Version
Changed by Joe
2.0 New Changes

To avoid breaking changes, I would suggest creating a new class, e.g. PandocTable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants