Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ jobs:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: gradle/actions/wrapper-validation@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
6 changes: 3 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17', '21', '25' ]
java: [ '21', '25', '27' ]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
Expand Down
11 changes: 6 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
buildscript {
configurations.classpath {
resolutionStrategy {
// JGit 7.x removed GpgObjectSigner which JReleaser still depends on
// Fix https://github.com/jreleaser/jreleaser/issues/1643
force("org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r")
force("org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r")
}
}
}
Expand All @@ -12,16 +13,16 @@ plugins {
`maven-publish`
signing
id("com.diffplug.spotless") version "latest.release"
id("org.jreleaser") version "1.24.0"
id("org.jreleaser") version "1.26.0"
}

repositories {
mavenCentral()
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
Expand Down Expand Up @@ -140,7 +141,7 @@ spotless {
}

tasks.wrapper {
gradleVersion = "9.5.0"
gradleVersion = "9.8.1"
distributionType = Wrapper.DistributionType.ALL
}

Expand Down
4 changes: 2 additions & 2 deletions configuration-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ group = "de.cronn"
version = "0.0.1-SNAPSHOT"

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
| location | correct |
|-------------------------|---------|
| Sanku Maots'ai 三顾冒菜 | ✅ |
| Café | ❌ |
| Café | ❤️ |
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
| text |
|-----------------------------|
| Maotsai 31.9/kg \| 0.392 kg |
| first line<br>second line |
10 changes: 5 additions & 5 deletions gradle.lockfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.fasterxml.jackson.core:jackson-annotations:2.21=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.21.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.21.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.21.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.22=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.22.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.22.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.22.3=testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.22.3=testCompileClasspath,testRuntimeClasspath
com.googlecode.java-diff-utils:diffutils:1.3.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.18.3=testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=compileClasspath,testCompileClasspath
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-all.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
4 changes: 2 additions & 2 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 43 additions & 13 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ public MarkdownTable(List<String> header) {
}

public void addCell(Object value) {
currentRow.add(Objects.toString(value));
currentRow.add(escape(Objects.toString(value)));
}

// an unescaped pipe or line break would end the cell or the row
private static String escape(String value) {
return value.replace("|", "\\|").replace("\r\n", "<br>").replace("\n", "<br>");
}

public void addCells(Collection<? extends Object> values) {
Expand Down Expand Up @@ -98,10 +103,11 @@ public String toString() {
}

private List<Integer> computeColumnsWidths() {
List<Integer> widths = header.stream().map(String::length).collect(Collectors.toList());
List<Integer> widths =
header.stream().map(MarkdownTable::displayWidth).collect(Collectors.toList());
for (int i = 0; i < header.size(); i++) {
for (List<String> row : rows) {
widths.set(i, Math.max(row.get(i).length(), widths.get(i)));
widths.set(i, Math.max(displayWidth(row.get(i)), widths.get(i)));
}
}
return widths;
Expand All @@ -116,7 +122,50 @@ private void repeat(char ch, int times, StringBuilder sb) {
private void pad(String value, int size, StringBuilder sb) {
sb.append(' ');
sb.append(value);
repeat(' ', size - value.length(), sb);
repeat(' ', size - displayWidth(value), sb);
sb.append(' ');
}

/**
* Number of columns the value occupies in a monospace font: East Asian wide characters and emoji
* take two columns, combining marks and format characters (such as the zero width joiner) none.
*/
static int displayWidth(String value) {
int width = 0;
int i = 0;
while (i < value.length()) {
int codePoint = value.codePointAt(i);
i += Character.charCount(codePoint);
if (isZeroWidth(codePoint)) {
continue;
}
boolean emojiPresentation =
i < value.length() && value.charAt(i) == EMOJI_PRESENTATION_SELECTOR;
width += isWide(codePoint) || emojiPresentation ? 2 : 1;
}
return width;
}

private static final char EMOJI_PRESENTATION_SELECTOR = '\uFE0F';

private static boolean isZeroWidth(int codePoint) {
int type = Character.getType(codePoint);
return type == Character.NON_SPACING_MARK
|| type == Character.ENCLOSING_MARK
|| type == Character.FORMAT;
}

// approximates East_Asian_Width W and F, which the JDK does not provide
private static boolean isWide(int codePoint) {
if (Character.isEmojiPresentation(codePoint)) {
return true;
}
return switch (Character.UnicodeScript.of(codePoint)) {
case HAN, HIRAGANA, KATAKANA, HANGUL, BOPOMOFO, YI -> true;
default ->
Character.UnicodeBlock.of(codePoint) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| (codePoint >= 0xFF01 && codePoint <= 0xFF60) // fullwidth forms
|| (codePoint >= 0xFFE0 && codePoint <= 0xFFE6);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,36 @@ void shouldPadCells() {
+ "| Countrywide Powdered Coatings Ltd |\n"
+ "| Craft Textiles Ltd |");
}

@Test
void shouldPadCellsContainingWideCharacters() {
MarkdownTable markdownTable = new MarkdownTable(List.of("location", "correct"));
markdownTable.addRow("Sanku Maots'ai 三顾冒菜", "✅");
markdownTable.addRow("Café", "❌");
markdownTable.addRow("Cafe\u0301", "❤\uFE0F");

assertWithFile(markdownTable.toString(), FileExtensions.MD);
}

@Test
void shouldRenderCellsContainingPipesAndLineBreaks() {
MarkdownTable markdownTable = new MarkdownTable(List.of("text"));
markdownTable.addRow("Maotsai 31.9/kg | 0.392 kg");
markdownTable.addRow("first line\nsecond line");

assertWithFile(markdownTable.toString(), FileExtensions.MD);
}

@Test
void shouldComputeDisplayWidth() {
assertThat(MarkdownTable.displayWidth("abc")).isEqualTo(3);
assertThat(MarkdownTable.displayWidth("三顾冒菜")).isEqualTo(8);
assertThat(MarkdownTable.displayWidth("AB")).isEqualTo(4);
assertThat(MarkdownTable.displayWidth("한국어")).isEqualTo(6);
assertThat(MarkdownTable.displayWidth("✅❌")).isEqualTo(4);
assertThat(MarkdownTable.displayWidth("😀")).isEqualTo(2);
assertThat(MarkdownTable.displayWidth("✓✗")).isEqualTo(2);
assertThat(MarkdownTable.displayWidth("e\u0301")).isEqualTo(1);
assertThat(MarkdownTable.displayWidth("❤\uFE0F")).isEqualTo(2);
}
}
Loading