Skip to content

Commit

Permalink
Merge pull request #86 from opencastsoftware/text-wrap
Browse files Browse the repository at this point in the history
Add a WrapText node to enable wrapping long strings
  • Loading branch information
DavidGregory084 authored Jun 6, 2024
2 parents aa73d3a + c293808 commit 3ca95dc
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 28 deletions.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ testing {
implementation(libs.hamcrest)
implementation(libs.equalsVerifier)
implementation(libs.toStringVerifier)
implementation(libs.apacheCommonsText)
}
}
}
Expand Down Expand Up @@ -73,7 +74,7 @@ mavenPublishing {
}
}

tasks.withType<JavaCompile> {
tasks.compileJava {
// Target Java 8
options.release.set(8)
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
apacheCommonsText = "1.12.0"
apiGuardian = "1.1.2"
equalsVerifier = "3.16.1"
gradleJavaConventions = "0.1.4"
Expand All @@ -10,6 +11,7 @@ toStringVerifier = "1.4.8"

[libraries]
apiGuardian = { module = "org.apiguardian:apiguardian-api", version.ref = "apiGuardian" }
apacheCommonsText = { module = "org.apache.commons:commons-text", version.ref = "apacheCommonsText" }
equalsVerifier = { module = "nl.jqno.equalsverifier:equalsverifier", version.ref = "equalsVerifier" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
jqwik = { module = "net.jqwik:jqwik", version.ref = "jqwik" }
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/com/opencastsoftware/prettier4j/Doc.java
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,68 @@ public String toString() {
}
}

/**
* Represents a long text string which may be wrapped to fit within
* the preferred rendering width.
*/
public static class WrapText extends Doc {
private final String text;

WrapText(String text) {
this.text = text;
}

public String text() {
return text;
}

@Override
Doc flatten() {
return this;
}

@Override
boolean hasParams() {
return false;
}

@Override
boolean hasLineSeparators() {
return false;
}

@Override
public Doc bind(String name, Doc value) {
return this;
}

@Override
public Doc bind(Map<String, Doc> bindings) {
return this;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WrapText wrapText = (WrapText) o;
return Objects.equals(text, wrapText.text);
}

@Override
public int hashCode() {
return Objects.hashCode(text);
}

@Override
public String toString() {
return "WrapText{" +
"text='" + text + '\'' +
'}';
}

}

/**
* Represents the concatenation of two {@link Doc}s.
*/
Expand Down Expand Up @@ -1311,6 +1373,19 @@ public static Doc text(String text) {
return new Text(text);
}

/**
* Construct a {@link Doc} which attempts to wrap the input {@code text}
* to fit within the preferred rendering width.
*
* @param text the input String.
* @return a {@link Doc Doc} representing that {@link String}.
*/
public static Doc wrapText(String text) {
// By empty text equivalency law
if (text.isEmpty()) { return empty(); }
return new WrapText(text);
}

/**
* Construct a {@link Doc Doc} representing two alternative layouts for a document.
*
Expand Down Expand Up @@ -1689,6 +1764,71 @@ static Deque<Entry> normalize(Doc doc, RenderOptions options, Doc margin, int in
altDoc.left(), altDoc.right(),
options, entryMargin, entryIndent, position);
chosenEntries.forEach(outQueue::addLast);
} else if (entryDoc instanceof WrapText) {
WrapText wrapDoc = (WrapText) entryDoc;
String wrapText = wrapDoc.text();
int textLength = wrapText.length();
if (textLength == 0) { continue; }

StringBuilder wrapped = new StringBuilder(options.lineWidth());

int textOffset = 0;
int wordStart = -1;
for (; textOffset < textLength; textOffset++) {
char currentChar = wrapText.charAt(textOffset);

boolean isInWord = wordStart >= 0;
boolean isWhitespace = Character.isWhitespace(currentChar);
boolean isStartOfWord = !isWhitespace && !isInWord;

if (isStartOfWord) {
wordStart = textOffset;
isInWord = true;
}

boolean isLastChar = textOffset == textLength - 1;
boolean isEndOfWord = (isWhitespace || isLastChar) && isInWord;
boolean isFirstWord = wrapped.length() == 0;

if (isEndOfWord) {
int precedingSpaces = isFirstWord ? 0 : 1;
int wordEnd = isLastChar ? textLength : textOffset;
int wordLength = wordEnd - wordStart + precedingSpaces;
int remaining = options.lineWidth() - position;
if (remaining < wordLength) {
if (isFirstWord) {
// It's a really long word, so send it out to make progress
wrapped.append(wrapText, wordStart, wordEnd);
position += wordLength;
wordStart = -1;
}
if (!isLastChar) break;
} else {
if (!isFirstWord) { wrapped.append(' '); }
wrapped.append(wrapText, wordStart, wordEnd);
position += wordLength;
wordStart = -1;
}
}
}

// Skip trailing whitespace
while (textOffset < textLength && Character.isWhitespace(wrapText.charAt(textOffset))) {
textOffset++;
}

int restOffset = wordStart > 0 ? wordStart : textOffset;
int remainingChars = textLength - restOffset;
if (remainingChars > 0) {
// Send out remainder prefixed by line separator
String remainingText = wrapText.substring(restOffset);
inQueue.addFirst(entry(entryIndent, entryMargin, wrapText(remainingText)));
inQueue.addFirst(entry(entryIndent, entryMargin, line()));
}

// Send out the wrapped line
inQueue.addFirst(entry(entryIndent, entryMargin, text(wrapped.toString())));

} else if (entryDoc instanceof Text) {
Text textDoc = (Text) entryDoc;
// Keep track of line length
Expand Down
Loading

0 comments on commit 3ca95dc

Please sign in to comment.