Skip to content

Commit 74814f7

Browse files
committed
Improve Markdown Table formatting
This commit aligns markdown table properly based on max length cell data
1 parent e22f9eb commit 74814f7

File tree

4 files changed

+196
-84
lines changed

4 files changed

+196
-84
lines changed

org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterMarkdownCommentsTests.java

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -467,60 +467,6 @@ class m22 {
467467
formatSource(input, expected);
468468
}
469469

470-
public void testMarkdownDoNotBreakTableOutsideClass() throws JavaModelException {
471-
setComplianceLevel(CompilerOptions.VERSION_23);
472-
String input = """
473-
/// | Latin | Greek |
474-
/// |-------|-------|
475-
/// | a | alpha |
476-
/// | b | beta |
477-
/// | c | gamma |
478-
class Main {}
479-
""";
480-
String expected = """
481-
/// | Latin | Greek |
482-
/// |-------|-------|
483-
/// | a | alpha |
484-
/// | b | beta |
485-
/// | c | gamma |
486-
class Main {
487-
}
488-
""";
489-
formatSource(input, expected);
490-
}
491-
public void testMarkdownDoNotBreakMultipleTableInsideClass() throws JavaModelException {
492-
setComplianceLevel(CompilerOptions.VERSION_23);
493-
String input = """
494-
class Main {
495-
/// | Latin | Greek |
496-
/// |-------|-------|
497-
/// | a | alpha |
498-
///
499-
/// Hello Eclipse
500-
///
501-
/// | Latin | Greek |
502-
/// |-------|-------|
503-
/// | a | alpha |
504-
public void sample(String param1) {}}
505-
""";
506-
String expected = """
507-
class Main {
508-
/// | Latin | Greek |
509-
/// |-------|-------|
510-
/// | a | alpha |
511-
///
512-
/// Hello Eclipse
513-
///
514-
/// | Latin | Greek |
515-
/// |-------|-------|
516-
/// | a | alpha |
517-
public void sample(String param1) {
518-
}
519-
}
520-
""";
521-
formatSource(input, expected);
522-
}
523-
524470
public void testMarkdownDoNotBreakTwoListOfSameLevel() throws JavaModelException {
525471
setComplianceLevel(CompilerOptions.VERSION_23);
526472
String input = """
@@ -880,5 +826,4 @@ class Mark61 {
880826
""";
881827
formatSource(input, expected);
882828
}
883-
884829
}

org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java

Lines changed: 151 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ public class CommentsPreparator extends ASTVisitor {
9696
private final static Pattern MARKDOWN_HEADINGS_PATTERN_1 = Pattern.compile("(?:(?<=^)|(?<=///[ \\t]*))(#{1,6})([ \\t]+)([^\\r\\n]*)"); //$NON-NLS-1$
9797
private final static Pattern MARKDOWN_HEADINGS_PATTERN_2 = Pattern.compile("(?:^|(?<=///[ \\t]+))[ \\t]*([=-])\\1*[ \\t]*(?=\\r?\\n|$)"); //$NON-NLS-1$
9898
private final static Pattern MARKDOWN_FENCE_PATTERN = Pattern.compile("(`{3,}|~{3,})(.*)"); //$NON-NLS-1$
99-
private final static Pattern MARKDOWN_TABLE_START = Pattern.compile("(?m)(?<=^[ \\t]*)\\|"); //$NON-NLS-1$
100-
private final static Pattern MARKDOWN_TABLE_END = Pattern.compile("(?m)\\|(?!.*\\|)"); //$NON-NLS-1$
99+
private final static Pattern MARKDOWN_TABLE_COLUMN_SEP = Pattern.compile("\\||(?<=\\|)\\s*-+\\s*(?=\\|)"); //$NON-NLS-1$
100+
private final static Pattern MARKDOWN_TABLE_COLUMN_VALIDATOR = Pattern.compile("^[ :\\-|]+$"); //$NON-NLS-1$
101101

102102
// Param tags list copied from IJavaDocTagConstants in legacy formatter for compatibility.
103103
// There were the following comments:
@@ -134,6 +134,7 @@ public class CommentsPreparator extends ASTVisitor {
134134
private final ArrayList<Integer> commonAttributeAnnotations = new ArrayList<>();
135135
private DefaultCodeFormatter preTagCodeFormatter;
136136
private DefaultCodeFormatter snippetCodeFormatter;
137+
private String markdownTablePipe = "|"; //$NON-NLS-1$
137138

138139
public CommentsPreparator(TokenManager tm, DefaultCodeFormatterOptions options, String sourceLevel) {
139140
this.tm = tm;
@@ -998,39 +999,162 @@ && formatCode(codeBlockStartIndex, codeBlockEndIndex + 1, false, true)) {
998999
}
9991000

10001001
private void handleMarkdownTable(List<ASTNode> fragments) {
1001-
int tableStartIndex = -1;
1002-
int tableLastIndex = -1;
1003-
boolean columnUnderlineFound = false;
10041002
Matcher matcher;
1005-
for (Object fragment : fragments) {
1006-
if (fragment instanceof TextElement textElement) {
1003+
ASTNode columnHeader = null;
1004+
boolean hasFormattedColumnHeader = false;
1005+
int maxRowDataLen = 0;
1006+
List<Token> columnHeaderTokens = new ArrayList<>();
1007+
List<Token> columnSeperatorTokens = new ArrayList<>();
1008+
for (int i = 0; i < fragments.size(); i++) {
1009+
if (fragments.get(i) instanceof TextElement textElement) {
10071010
String textContent = textElement.getText();
1008-
matcher = MARKDOWN_TABLE_START.matcher(textContent);
1009-
if (matcher.find()) {
1010-
if (tableStartIndex == -1) {
1011-
int startPos = matcher.start() + textElement.getStartPosition();
1012-
tableStartIndex = tokenStartingAt(startPos);
1013-
} else if (tableStartIndex != -1 && !columnUnderlineFound) {
1014-
boolean foundStart = textContent.contains("|-"); //$NON-NLS-1$
1015-
boolean foundEnd = textContent.contains("-|"); //$NON-NLS-1$
1016-
if (foundStart && foundEnd) {
1017-
columnUnderlineFound = true;
1011+
if (columnSeperatorTokens.isEmpty()) {
1012+
matcher = MARKDOWN_TABLE_COLUMN_VALIDATOR.matcher(textContent);
1013+
if (matcher.find()) {
1014+
columnHeader = fragments.get(i - 1);
1015+
// find the most lengthy cell data
1016+
for (int rowIndex = i; rowIndex < fragments.size(); rowIndex++) {
1017+
TextElement element = (TextElement) fragments.get(rowIndex);
1018+
String rowData = element.getText();
1019+
int maxRow = Arrays.stream(rowData.split("\\|")).map(e -> e.trim()) //$NON-NLS-1$
1020+
.filter(s -> !s.isEmpty()).mapToInt(String::length).max().orElse(0);
1021+
maxRowDataLen = Math.max(maxRow, maxRowDataLen);
1022+
}
1023+
String headData = ((TextElement) columnHeader).getText();
1024+
int maxHead = Arrays.stream(headData.split("\\|")).map(e -> e.trim()) //$NON-NLS-1$
1025+
.filter(s -> !s.isEmpty()).mapToInt(String::length).max().orElse(0);
1026+
maxRowDataLen = maxHead == maxRowDataLen ? maxRowDataLen + 2
1027+
: Math.max(maxHead, maxRowDataLen + 2);
1028+
matcher = MARKDOWN_TABLE_COLUMN_SEP.matcher(textContent);
1029+
while (matcher.find()) {
1030+
int startPos = matcher.start() + fragments.get(i).getStartPosition();
1031+
int tokenIndex = this.ctm.findIndex(startPos, ANY, true);
1032+
Token columnSeperatorToken = this.ctm.get(tokenIndex);
1033+
columnSeperatorToken.setWrapPolicy(WrapPolicy.DISABLE_WRAP);
1034+
columnSeperatorToken.setColumnSeparator(true);
1035+
columnSeperatorTokens.add(columnSeperatorToken);
1036+
}
1037+
if (!columnSeperatorTokens.isEmpty()) {
1038+
columnSeperatorTokens.get(columnSeperatorTokens.size() - 1).breakAfter();
10181039
}
1019-
} else if (columnUnderlineFound) {
1020-
matcher = MARKDOWN_TABLE_END.matcher(textContent);
1021-
matcher.find(); // find the last one
1040+
}
1041+
} else if (!columnSeperatorTokens.isEmpty() && !hasFormattedColumnHeader) {
1042+
String columnString = ((TextElement) columnHeader).getText();
1043+
Pattern p = Pattern.compile("\\||(?<=\\||\\s)[^|\\s]+(?=\\s|\\||$)"); //$NON-NLS-1$
1044+
matcher = p.matcher(columnString);
1045+
while (matcher.find()) {
1046+
int startPos = matcher.start() + columnHeader.getStartPosition();
1047+
int tokenIndex = this.ctm.findIndex(startPos, ANY, true);
1048+
Token columnToken = this.ctm.get(tokenIndex);
1049+
columnToken.setWrapPolicy(WrapPolicy.DISABLE_WRAP);
1050+
String content = this.ctm.toString(columnToken);
1051+
if (!content.equals(this.markdownTablePipe)) {
1052+
columnToken.setMarkdownColumnHeader(true);
1053+
}
1054+
columnHeaderTokens.add(columnToken);
1055+
}
1056+
columnHeaderTokens.get(columnSeperatorTokens.size() - 1).breakAfter();
1057+
formatMarkdownTableHeader(columnHeaderTokens, columnSeperatorTokens, maxRowDataLen);
1058+
hasFormattedColumnHeader = true;
1059+
}
1060+
if (hasFormattedColumnHeader) {
1061+
List<Token> rowTokens = new ArrayList<>();
1062+
String rowSet = textContent;
1063+
Pattern p = Pattern.compile("\\||(?<=\\||^)[^|]+(?=\\||$)"); //$NON-NLS-1$
1064+
matcher = p.matcher(rowSet);
1065+
while (matcher.find()) {
10221066
int startPos = matcher.start() + textElement.getStartPosition();
1023-
tableLastIndex = tokenStartingAt(startPos);
1067+
int tokenIndex = this.ctm.findIndex(startPos, ANY, true);
1068+
if (tokenIndex >= this.ctm.size()) {
1069+
continue;
1070+
}
1071+
Token rowToken = this.ctm.get(tokenIndex);
1072+
rowToken.setWrapPolicy(WrapPolicy.DISABLE_WRAP);
1073+
rowTokens.add(rowToken);
10241074
}
1075+
rowTokens.get(rowTokens.size() - 1).breakAfter();
1076+
formatMarkdownTableRow(rowTokens, maxRowDataLen);
1077+
}
1078+
}
1079+
}
1080+
}
1081+
1082+
private void formatMarkdownTableHeader(List<Token> columnHeaderTokens, List<Token> columnSeperatorTokens,
1083+
int maxRowDataLen) {
1084+
Token cellStart = null;
1085+
Token cellEnd = null;
1086+
List<Token> cellContent = new ArrayList<>();
1087+
int cel = 0;
1088+
for (int j = 0; j < columnHeaderTokens.size(); j++) {
1089+
Token columnToken = columnHeaderTokens.get(j);
1090+
;
1091+
if (!columnToken.isMarkdownColumnHeader() && cellStart == null) {
1092+
cellStart = columnToken;
1093+
} else if (!columnToken.isMarkdownColumnHeader() && cellEnd == null) {
1094+
cellEnd = columnToken;
1095+
} else {
1096+
cellContent.add(columnToken);
1097+
}
1098+
if (cellStart != null && cellEnd != null) {
1099+
cellStart.spaceAfter();
1100+
cellContent.get(cellContent.size() - 1).clearSpaceAfter();
1101+
cellContent.get(0).clearSpaceBefore();
1102+
int contentLength = cellContent.stream().mapToInt(t -> this.tm.toString(t).length()).sum();
1103+
contentLength = cellContent.size() > 1 ? contentLength + cellContent.size() - 1 : contentLength;
1104+
int newLen = (maxRowDataLen - contentLength);
1105+
int prev = cellStart.getAlign() == 0 ? 4 : cellStart.getAlign();
1106+
Token previous = cellContent.get(0);
1107+
if (cellContent.size() == 1) {
1108+
cellContent.get(0).setAlign(prev + (newLen / 2) + 1);
10251109
} else {
1026-
tableStartIndex = -1;
1027-
tableLastIndex = -1;
1028-
columnUnderlineFound = false;
1110+
cellContent.get(0).setAlign(prev + (newLen / 2) + 1);
1111+
if (cellContent.size() > 1) {
1112+
for (int i = 1; i < cellContent.size(); i++) {
1113+
Token tmp = cellContent.get(i);
1114+
tmp.setAlign(previous.getAlign());
1115+
previous = tmp;
1116+
cel++;
1117+
contentLength = this.tm.toString(previous).length();
1118+
}
1119+
}
10291120
}
1121+
cellEnd.setAlign(previous.getAlign() + contentLength + ((newLen / 2)));
1122+
cellStart = cellEnd;
1123+
cellEnd = null;
1124+
int toBeAdded = this.tm.toString(columnSeperatorTokens.get(j - 1 - cel)).length();
1125+
int padding = contentLength % 2 == 0 ? 0 : -1;
1126+
columnSeperatorTokens.get(j - 1 - cel).setMarkdownColumnLength(maxRowDataLen - toBeAdded + padding);
1127+
cellContent.clear();
1128+
}
1129+
}
1130+
}
1131+
1132+
private void formatMarkdownTableRow(List<Token> rowTokens, int maxRowDataLen) {
1133+
Token cellStart = null;
1134+
Token cellEnd = null;
1135+
Token cellContent = null;
1136+
for (int j = 0; j < rowTokens.size(); j++) {
1137+
Token columnToken = rowTokens.get(j);
1138+
String colVal = this.tm.toString(columnToken);
1139+
if (colVal.equals(this.markdownTablePipe) && cellStart == null) {
1140+
cellStart = columnToken;
1141+
} else if (colVal.equals(this.markdownTablePipe) && cellEnd == null) {
1142+
cellEnd = columnToken;
1143+
} else {
1144+
cellContent = columnToken;
10301145
}
1031-
if (tableStartIndex != -1 && tableLastIndex != -1) {
1032-
// TODO fix column alignment and format cells
1033-
disableFormattingExclusively(tableStartIndex, tableLastIndex);
1146+
if (cellStart != null && cellEnd != null) {
1147+
cellContent.clearSpaceAfter();
1148+
cellContent.clearSpaceBefore();
1149+
int contentLength = this.tm.toString(cellContent).length();
1150+
int newLen = (maxRowDataLen - contentLength);
1151+
cellStart.spaceAfter();
1152+
int prev = cellStart.getAlign() == 0 ? 0 : cellStart.getAlign();
1153+
int padding = contentLength == maxRowDataLen ? 0 : 1;
1154+
cellContent.setAlign(prev + (newLen / 2) + padding);
1155+
cellEnd.setAlign(cellContent.getAlign() + contentLength + ((newLen / 2)));
1156+
cellStart = cellEnd;
1157+
cellEnd = null;
10341158
}
10351159
}
10361160
}

org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/TextEditsBuilder.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,29 @@ public static void appendIndentationString(StringBuilder target, int tabChar, in
304304
}
305305

306306
private boolean bufferAlign(Token token, int index) {
307+
boolean mdColNo = false;
308+
if(token.tokenType==TokenNameCOMMENT_MARKDOWN) {
309+
mdColNo = token.isColumnSeparator();
310+
int mdColLen = token.getMarkdownColumnLength();
311+
312+
if(mdColLen>0) {
313+
String col = this.tm.toString(token);
314+
if (col.contains("-")) {
315+
for (int i = 0; i < mdColLen; i++) {
316+
this.buffer.append("-");
317+
}
318+
}
319+
}
320+
}
307321
int align = token.getAlign();
322+
308323
int alignmentChar = this.alignChar;
309324
if (align == 0 && getLineBreaksBefore() == 0 && this.parent != null) {
310325
align = token.getIndent();
311326
token.setAlign(align);
312327
alignmentChar = DefaultCodeFormatterOptions.SPACE;
313328
}
314-
if (align == 0)
329+
if (align == 0 && !mdColNo)
315330
return false;
316331

317332
int currentPositionInLine = 0;
@@ -322,7 +337,7 @@ private boolean bufferAlign(Token token, int index) {
322337
currentPositionInLine = this.tm.getPositionInLine(index - 1);
323338
currentPositionInLine += this.tm.getLength(this.tm.get(index - 1), currentPositionInLine);
324339
}
325-
if (currentPositionInLine >= align)
340+
if (currentPositionInLine >= align && !mdColNo)
326341
return false;
327342

328343
final int tabSize = this.options.tab_size;

org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/Token.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ public WrapPolicy(WrapMode wrapMode, int wrapParentIndex, int extraIndent) {
107107

108108
private List<Token> internalStructure;
109109

110+
private boolean columnSeparator;
111+
private int markdownColumnLength;
112+
private boolean isMarkdownColumnHeader;
113+
110114
public Token(int sourceStart, int sourceEnd, TerminalToken tokenType) {
111115
assert sourceStart <= sourceEnd;
112116
this.originalStart = sourceStart;
@@ -339,6 +343,30 @@ public int countChars() {
339343
return this.originalEnd - this.originalStart + 1;
340344
}
341345

346+
public boolean isColumnSeparator() {
347+
return this.columnSeparator;
348+
}
349+
350+
public void setColumnSeparator(boolean columnSeparator) {
351+
this.columnSeparator = columnSeparator;
352+
}
353+
354+
public int getMarkdownColumnLength() {
355+
return this.markdownColumnLength;
356+
}
357+
358+
public void setMarkdownColumnLength(int markdownColumnLength) {
359+
this.markdownColumnLength = markdownColumnLength;
360+
}
361+
362+
public boolean isMarkdownColumnHeader() {
363+
return this.isMarkdownColumnHeader;
364+
}
365+
366+
public void setMarkdownColumnHeader(boolean isMarkdownColumnHeader) {
367+
this.isMarkdownColumnHeader = isMarkdownColumnHeader;
368+
}
369+
342370
/*
343371
* Conceptually, Token abstracts away from the source so it doesn't need to know how
344372
* the source looks like. However, it's useful to see the actual token contents while debugging.

0 commit comments

Comments
 (0)