bindingsMap = new HashMap<>();
+
+ for (int i = 0; i < bindings.length; i += 2) {
+ if (!(bindings[i] instanceof String)) {
+ throw new IllegalArgumentException(
+ "Key type must be String, but was " +
+ bindings[i].getClass().getSimpleName() +
+ " at index " + i + '.');
+ }
+ if (!(bindings[i + 1] instanceof Doc)) {
+ throw new IllegalArgumentException(
+ "Value type must be Doc, but was " +
+ bindings[i + 1].getClass().getSimpleName() +
+ " at index" + (i + 1) + '.');
+ }
+
+ bindingsMap.put(
+ (String) bindings[i],
+ (Doc) bindings[i + 1]);
+ }
+
+ return bind(bindingsMap);
+ }
+
+ /**
+ * Bind a named parameter to the {@link String} provided via {@code value}.
+ *
+ * @return this {@link Doc} with all instances of the named parameter replaced by {@code value}.
+ */
+ public Doc bind(String name, String value) {
+ return bind(name, Doc.text(value));
+ }
+
/**
* Append the {@code other} {@link com.opencastsoftware.prettier4j.Doc Doc} to
* this one.
@@ -247,16 +318,34 @@ public final Doc styled(Styles.StylesOperator...styles) {
* Renders the current {@link com.opencastsoftware.prettier4j.Doc Doc} into a
* {@link java.lang.String String}, aiming to lay out the document with at most
* {@code width} characters on each line.
- *
- * By default, ANSI escape codes are rendered to the output {@link java.lang.String String}.
- *
- * To disable ANSI escape codes, see the {@link Doc#render(int, boolean)} overload of render.
*
- * @param width the preferred maximum rendering width.
* @return the document laid out as a {@link java.lang.String String}.
*/
- public String render(int width) {
- return render(width, this);
+ public String render() {
+ return render(this);
+ }
+
+ /**
+ * Renders the current {@link com.opencastsoftware.prettier4j.Doc Doc} into a
+ * {@link java.lang.String String}, aiming to lay out the document with at most
+ * {@code width} characters on each line.
+ *
+ * @param options the options to use for rendering.
+ * @return the document laid out as a {@link java.lang.String String}.
+ */
+ public String render(RenderOptions options) {
+ return render(this, options);
+ }
+
+ /**
+ * Renders the current {@link com.opencastsoftware.prettier4j.Doc Doc} into a
+ * {@link java.lang.String String}, aiming to lay out the document with at most
+ * {@code width} characters on each line.
+ *
+ * @return the document laid out as a {@link java.lang.String String}.
+ */
+ public void render(Appendable output) throws IOException {
+ render(this, output);
}
/**
@@ -265,11 +354,10 @@ public String render(int width) {
* {@code width} characters on each line.
*
* @param width the preferred maximum rendering width.
- * @param ansi whether to render ANSI escape codes.
* @return the document laid out as a {@link java.lang.String String}.
*/
- public String render(int width, boolean ansi) {
- return render(width, ansi, this);
+ public String render(int width) {
+ return render(this, new RenderOptions(width, true));
}
/**
@@ -278,29 +366,24 @@ public String render(int width, boolean ansi) {
* {@code width} characters on each line.
*
* @param width the preferred maximum rendering width.
- * @param ansi whether to render ANSI escape codes.
* @param output the output to render into.
* @throws IOException if the {@link Appendable} {@code output} throws when {@link Appendable#append(CharSequence) append}ed.
*/
- public void render(int width, boolean ansi, Appendable output) throws IOException {
- render(width, ansi, this, output);
+ public void render(int width, Appendable output) throws IOException {
+ render(this, new RenderOptions(width, true), output);
}
/**
* Renders the current {@link com.opencastsoftware.prettier4j.Doc Doc} into an
* {@link java.lang.Appendable Appendable}, aiming to lay out the document with at most
* {@code width} characters on each line.
- *
- * By default, ANSI escape codes are rendered to the {@code output}.
- *
- * To disable ANSI escape codes, see the {@link Doc#render(int, boolean, Appendable)} overload of render.
*
- * @param width the preferred maximum rendering width.
+ * @param options the options to use for rendering.
* @param output the output to render into.
* @throws IOException if the {@link Appendable} {@code output} throws when {@link Appendable#append(CharSequence) append}ed.
*/
- public void render(int width, Appendable output) throws IOException {
- render(width, this, output);
+ public void render(RenderOptions options, Appendable output) throws IOException {
+ render(this, options, output);
}
/**
@@ -322,6 +405,21 @@ Doc flatten() {
return this;
}
+ @Override
+ boolean hasParams() {
+ return false;
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public int hashCode() {
final int prime = 31;
@@ -379,6 +477,21 @@ Doc flatten() {
return left.flatten().append(right.flatten());
}
+ @Override
+ boolean hasParams() {
+ return left.hasParams() || right.hasParams();
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return new Append(left.bind(name, value), right.bind(name, value));
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return new Append(left.bind(bindings), right.bind(bindings));
+ }
+
@Override
public int hashCode() {
final int prime = 31;
@@ -458,6 +571,21 @@ Doc flatten() {
return left.flatten();
}
+ @Override
+ boolean hasParams() {
+ return left.hasParams() || right.hasParams();
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return new Alternatives(left.bind(name, value), right.bind(name, value));
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return new Alternatives(left.bind(bindings), right.bind(bindings));
+ }
+
@Override
public int hashCode() {
final int prime = 31;
@@ -520,6 +648,21 @@ Doc flatten() {
return doc.flatten().indent(indent);
}
+ @Override
+ boolean hasParams() {
+ return doc.hasParams();
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return new Indent(indent, doc.bind(name, value));
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return new Indent(indent, doc.bind(bindings));
+ }
+
@Override
public int hashCode() {
final int prime = 31;
@@ -569,6 +712,16 @@ public Line() {
super();
}
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public String toString() {
return "Line []";
@@ -587,6 +740,16 @@ public LineOrEmpty() {
super(empty());
}
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public String toString() {
return "LineOrEmpty []";
@@ -607,6 +770,16 @@ static LineOrSpace getInstance() {
super(text(" "));
}
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public String toString() {
return "LineOrSpace []";
@@ -630,7 +803,22 @@ protected LineOr(Doc altDoc) {
@Override
Doc flatten() {
- return altDoc;
+ return altDoc != this ? altDoc.flatten() : altDoc;
+ }
+
+ @Override
+ boolean hasParams() {
+ return altDoc != this && altDoc.hasParams();
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return new LineOr(altDoc.bind(name, value));
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return new LineOr(altDoc.bind(bindings));
}
@Override
@@ -696,6 +884,21 @@ Doc flatten() {
return this;
}
+ @Override
+ boolean hasParams() {
+ return false;
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public String toString() {
return "Empty []";
@@ -727,6 +930,21 @@ Doc flatten() {
return new Styled(doc.flatten(), styles);
}
+ @Override
+ boolean hasParams() {
+ return doc.hasParams();
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return new Styled(doc.bind(name, value), styles);
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return new Styled(doc.bind(bindings), styles);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -768,6 +986,21 @@ Doc flatten() {
return this;
}
+ @Override
+ boolean hasParams() {
+ return false;
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -790,7 +1023,7 @@ public String toString() {
}
/**
- * Represents the end of a Doc that is {@link Doc#styled(Styles.StylesOperator...) styled} with an ANSI escape code sequence.
+ * Represents the end of a {@link Doc} that is {@link Doc#styled(Styles.StylesOperator...) styled} with an ANSI escape code sequence.
*/
public static class Reset extends Doc {
private static final Reset INSTANCE = new Reset();
@@ -807,12 +1040,98 @@ Doc flatten() {
return this;
}
+ @Override
+ boolean hasParams() {
+ return false;
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ return this;
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ return this;
+ }
+
@Override
public String toString() {
return "Reset []";
}
}
+ /**
+ * Represents a placeholder for a {@link Doc} that will be provided as a parameter.
+ */
+ public static class Param extends Doc {
+ private final String name;
+ private final boolean flattened;
+
+ private Param(String name, boolean flattened) {
+ this.name = name;
+ this.flattened = flattened;
+ }
+
+ Param(String name) {
+ this(name, false);
+ }
+
+ public String name() {
+ return this.name;
+ }
+
+ @Override
+ Doc flatten() {
+ return new Param(name, true);
+ }
+
+ @Override
+ boolean hasParams() {
+ return true;
+ }
+
+ @Override
+ Doc bind(String name, Doc value) {
+ if (this.name.equals(name)) {
+ return flattened ? value.flatten() : value;
+ } else {
+ return this;
+ }
+ }
+
+ @Override
+ Doc bind(Map bindings) {
+ Doc value = bindings.getOrDefault(this.name, this);
+ if (value != this) {
+ return flattened ? value.flatten() : value;
+ } else {
+ return this;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Param param = (Param) o;
+ return flattened == param.flattened && Objects.equals(name, param.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, flattened);
+ }
+
+ @Override
+ public String toString() {
+ return "Param[" +
+ "name='" + name + '\'' +
+ ", flattened=" + flattened +
+ ']';
+ }
+ }
+
/**
* Construct a {@link com.opencastsoftware.prettier4j.Doc Doc} from the
* {@code text}.
@@ -931,6 +1250,16 @@ public static Doc styled(Doc doc, Styles.StylesOperator ...styles) {
return new Styled(doc, styles);
}
+ /**
+ * Creates a {@link Doc} which acts as a placeholder for a parameter {@link Doc} that will be provided
+ * by {@link Doc#bind(String, Doc) binding} parameters prior to {@link Doc#render(int) render}ing.
+ * @param name the name of the parameter.
+ * @return a parameter {@link Doc}.
+ */
+ public static Doc param(String name) {
+ return new Param(name);
+ }
+
/**
* Reduce a collection of documents using the binary operator {@code fn},
* returning an empty document if the collection is empty.
@@ -1035,16 +1364,23 @@ static boolean fits(int remaining, Deque> entries) {
* and line position, choosing the {@code left} document if it fits and the
* {@code right} document otherwise.
*
- * @param width the preferred maximum width for rendering.
- * @param indent the current indentation level.
- * @param position the position in the current line.
* @param left the preferred compact layout.
* @param right the expanded layout.
+ * @param options the options to use for rendering.
+ * @param indent the current indentation level.
+ * @param position the position in the current line.
* @return the entries of the chosen layout.
*/
- static Deque> chooseLayout(int width, boolean ansi, int indent, int position, Doc left, Doc right) {
- Deque> leftEntries = normalize(width, ansi, indent, position, left);
- return fits(width - position, leftEntries) ? leftEntries : normalize(width, ansi, indent, position, right);
+ static Deque> chooseLayout(Doc left, Doc right, RenderOptions options, int indent, int position) {
+ Deque> leftEntries = normalize(left, options, indent, position);
+
+ int remaining = options.lineWidth() - position;
+
+ if (fits(remaining, leftEntries)) {
+ return leftEntries;
+ } else {
+ return normalize(right, options, indent, position);
+ }
}
/**
@@ -1053,14 +1389,13 @@ static Deque> chooseLayout(int width, boolean ansi, int
* {@link com.opencastsoftware.prettier4j.Doc.LineOr LineOr}, and producing a
* queue of entries to be rendered.
*
- * @param width the preferred maximum width for rendering.
- * @param ansi whether to render ANSI escape codes.
+ * @param doc the document to be rendered.
+ * @param options the options to use for rendering.
* @param indent the current indentation level.
* @param position the current position in the line.
- * @param doc the document to be rendered.
* @return a queue of entries to be rendered.
*/
- static Deque> normalize(int width, boolean ansi, int indent, int position, Doc doc) {
+ static Deque> normalize(Doc doc, RenderOptions options, int indent, int position) {
// Not yet normalized entries
Deque> inQueue = new ArrayDeque<>();
@@ -1085,7 +1420,7 @@ static Deque> normalize(int width, boolean ansi, int ind
} else if (entryDoc instanceof Styled) {
// Eliminate Styled
Styled styledDoc = (Styled) entryDoc;
- if (ansi) {
+ if (options.emitAnsiEscapes()) {
// Note reverse order
inQueue.addFirst(new SimpleEntry<>(entryIndent, Reset.getInstance()));
inQueue.addFirst(new SimpleEntry<>(entryIndent, styledDoc.doc()));
@@ -1104,7 +1439,7 @@ static Deque> normalize(int width, boolean ansi, int ind
Alternatives altDoc = (Alternatives) entryDoc;
// These entries are already normalized
Deque> chosenEntries = chooseLayout(
- width, ansi, entryIndent, position, altDoc.left(), altDoc.right());
+ altDoc.left(), altDoc.right(), options, entryIndent, position);
// Note reverse order
chosenEntries.descendingIterator().forEachRemaining(inQueue::addFirst);
} else if (entryDoc instanceof Text) {
@@ -1129,35 +1464,18 @@ static Deque> normalize(int width, boolean ansi, int ind
/**
* Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into an
- * {@link java.lang.Appendable Appendable}, aiming to lay out the document with at most
- * {@code width} characters on each line.
- *
- * By default, ANSI escape codes are rendered to the {@code output}.
- *
- * To disable ANSI escape codes, see the {@link Doc#render(int, boolean, Doc, Appendable)} overload of render.
+ * {@link java.lang.Appendable Appendable}, attempting to lay out the document
+ * according to the rendering {@code options}.
*
- * @param width the preferred maximum rendering width.
- * @param doc the document to be rendered.
- * @param output the output to render into.
+ * @param doc the document to be rendered.
+ * @param options the options to use for rendering.
+ * @param output the output to render into.
* @throws IOException if the {@link Appendable} {@code output} throws when {@link Appendable#append(CharSequence) append}ed.
*/
- public static void render(int width, Doc doc, Appendable output) throws IOException {
- render(width, true, doc, output);
- }
+ public static void render(Doc doc, RenderOptions options, Appendable output) throws IOException {
+ if (doc.hasParams()) { throw new IllegalStateException("This Doc contains unbound parameters"); }
- /**
- * Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into an
- * {@link java.lang.Appendable Appendable}, aiming to lay out the document with at most
- * {@code width} characters on each line.
- *
- * @param width the preferred maximum rendering width.
- * @param ansi whether to render ANSI escape codes.
- * @param doc the document to be rendered.
- * @param output the output to render into.
- * @throws IOException if the {@link Appendable} {@code output} throws when {@link Appendable#append(CharSequence) append}ed.
- */
- public static void render(int width, boolean ansi, Doc doc, Appendable output) throws IOException {
- Deque> renderQueue = normalize(width, ansi, 0, 0, doc);
+ Deque> renderQueue = normalize(doc, options, 0, 0);
AttrsStack attrsStack = new AttrsStack();
for (Map.Entry entry : renderQueue) {
@@ -1171,7 +1489,7 @@ public static void render(int width, boolean ansi, Doc doc, Appendable output) t
} else if (entryDoc instanceof LineOr) {
output.append(System.lineSeparator());
for (int i = 0; i < entryIndent; i++) {
- output.append(" ");
+ output.append(' ');
}
} else if (entryDoc instanceof Reset) {
long resetAttrs = attrsStack.popLast();
@@ -1189,41 +1507,48 @@ public static void render(int width, boolean ansi, Doc doc, Appendable output) t
}
/**
- * Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into a
- * {@link java.lang.String String}, aiming to lay out the document with at most
- * {@code width} characters on each line.
- *
- * By default, ANSI escape codes are rendered to the output {@link java.lang.String String}.
- *
- * To disable ANSI escape codes, see the {@link Doc#render(int, boolean, Doc)} overload of render.
+ * Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into an
+ * {@link java.lang.Appendable Appendable}, attempting to lay out the document
+ * according to the {@link RenderOptions#defaults() default} rendering options.
*
- * @param width the preferred maximum rendering width.
- * @param doc the document to be rendered.
- * @return the document laid out as a {@link java.lang.String String}.
+ * @param doc the document to be rendered.
+ * @param output the output to render into.
+ * @throws IOException if the {@link Appendable} {@code output} throws when {@link Appendable#append(CharSequence) append}ed.
*/
- public static String render(int width, Doc doc) {
- return render(width, true, doc);
+ public static void render(Doc doc, Appendable output) throws IOException {
+ render(doc, RenderOptions.defaults(), output);
}
/**
* Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into a
- * {@link java.lang.String String}, aiming to lay out the document with at most
- * {@code width} characters on each line.
+ * {@link java.lang.String String}, attempting to lay out the document
+ * according to the rendering {@code options}.
*
- * @param width the preferred maximum rendering width.
- * @param ansi whether to render ANSI escape codes.
- * @param doc the document to be rendered.
+ * @param doc the document to be rendered.
+ * @param options the options to use for rendering.
* @return the document laid out as a {@link java.lang.String String}.
*/
- public static String render(int width, boolean ansi, Doc doc) {
+ public static String render(Doc doc, RenderOptions options) {
StringBuilder output = new StringBuilder();
try {
- render(width, ansi, doc, output);
+ render(doc, options, output);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
return output.toString();
}
+
+ /**
+ * Renders the input {@link com.opencastsoftware.prettier4j.Doc Doc} into a
+ * {@link java.lang.String String}, attempting to lay out the document
+ * according to the {@link RenderOptions#defaults() default} rendering options.
+ *
+ * @param doc the document to be rendered.
+ * @return the document laid out as a {@link java.lang.String String}.
+ */
+ public static String render(Doc doc) {
+ return render(doc, RenderOptions.defaults());
+ }
}
diff --git a/src/main/java/com/opencastsoftware/prettier4j/RenderOptions.java b/src/main/java/com/opencastsoftware/prettier4j/RenderOptions.java
new file mode 100644
index 0000000..f777c82
--- /dev/null
+++ b/src/main/java/com/opencastsoftware/prettier4j/RenderOptions.java
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: © 2024 Opencast Software Europe Ltd
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.opencastsoftware.prettier4j;
+
+import java.util.Objects;
+
+public class RenderOptions {
+ private final int lineWidth;
+ private final boolean emitAnsiEscapes;
+
+ RenderOptions(int lineWidth, boolean emitAnsiEscapes) {
+ this.lineWidth = lineWidth;
+ this.emitAnsiEscapes = emitAnsiEscapes;
+ }
+
+ public int lineWidth() {
+ return this.lineWidth;
+ }
+
+ public boolean emitAnsiEscapes() {
+ return this.emitAnsiEscapes;
+ }
+
+ public static RenderOptions defaults() {
+ return new RenderOptions(80, true);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RenderOptions that = (RenderOptions) o;
+ return lineWidth == that.lineWidth && emitAnsiEscapes == that.emitAnsiEscapes;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lineWidth, emitAnsiEscapes);
+ }
+
+ @Override
+ public String toString() {
+ return "RenderOptions[" +
+ "lineWidth=" + lineWidth +
+ ", emitAnsiEscapes=" + emitAnsiEscapes +
+ ']';
+ }
+
+ public static class Builder {
+ private int lineWidth;
+ private boolean emitAnsiEscapes;
+ private Builder() {}
+
+ public RenderOptions build() {
+ return new RenderOptions(this.lineWidth, this.emitAnsiEscapes);
+ }
+
+ public Builder lineWidth(int width) {
+ this.lineWidth = width;
+ return this;
+ }
+
+ public Builder emitAnsiEscapes(boolean emitAnsi) {
+ this.emitAnsiEscapes = emitAnsi;
+ return this;
+ }
+ }
+}
diff --git a/src/test/java/com/opencastsoftware/prettier4j/DocTest.java b/src/test/java/com/opencastsoftware/prettier4j/DocTest.java
index aa89c00..8ae2a1b 100644
--- a/src/test/java/com/opencastsoftware/prettier4j/DocTest.java
+++ b/src/test/java/com/opencastsoftware/prettier4j/DocTest.java
@@ -18,11 +18,12 @@
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
+import java.util.function.UnaryOperator;
import static com.opencastsoftware.prettier4j.Doc.*;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
public class DocTest {
/**
@@ -911,15 +912,29 @@ void testRenderToAppendable() throws IOException {
assertThat(writer.toString(), is("a"));
}
+ @Test
+ void testRenderToAppendableDefaultOptions() throws IOException {
+ Doc doc = text("a");
+ Writer writer = new StringWriter();
+ doc.render(writer);
+ assertThat(writer.toString(), is("a"));
+ }
+
@Test
void testRenderAnsiDisabled() {
+ RenderOptions options = RenderOptions.builder()
+ .lineWidth(6)
+ .emitAnsiEscapes(false)
+ .build();
+
String expected = "(a, b)";
+
String actual = text("a").styled(Styles.blink())
.append(text(","))
.appendSpace(text("b").styled(Styles.blink()))
.bracket(2, Doc.lineOrEmpty(), text("("), text(")"))
.styled(Styles.faint())
- .render(6, false);
+ .render(options);
char[] expectedChars = expected.toCharArray();
char[] actualChars = actual.toCharArray();
@@ -930,6 +945,12 @@ void testRenderAnsiDisabled() {
@Test
void testRenderToAppendableAnsiDisabled() throws IOException {
Writer writer = new StringWriter();
+
+ RenderOptions options = RenderOptions.builder()
+ .lineWidth(6)
+ .emitAnsiEscapes(false)
+ .build();
+
String expected = "(a, b)";
text("a").styled(Styles.blink())
@@ -937,7 +958,7 @@ void testRenderToAppendableAnsiDisabled() throws IOException {
.appendSpace(text("b").styled(Styles.blink()))
.bracket(2, Doc.lineOrEmpty(), text("("), text(")"))
.styled(Styles.faint())
- .render(6, false, writer);
+ .render(options, writer);
char[] expectedChars = expected.toCharArray();
char[] actualChars = writer.toString().toCharArray();
@@ -945,6 +966,76 @@ void testRenderToAppendableAnsiDisabled() throws IOException {
assertThat(actualChars, is(equalTo(expectedChars)));
}
+ @Test
+ void testRenderWithUnboundParam() {
+ Doc unbound = Doc.param("a");
+ assertThrows(IllegalStateException.class, unbound::render);
+ }
+
+ @Test
+ void testRenderWithParamBoundToItself() {
+ Doc unbound = Doc.param("a");
+ Doc bound = unbound.bind("a", unbound);
+ assertThrows(IllegalStateException.class, bound::render);
+ }
+
+ @Test
+ void testRenderWithMultipleParams() {
+ Doc unbound = param("a")
+ .append(text(","))
+ .appendLineOrSpace(param("b"))
+ .bracket(2, lineOrEmpty(), text("("), text(")"));
+
+ String rendered = unbound.bind(
+ "a", text("1"),
+ "b", text("2"))
+ .render(80);
+
+ assertThat(rendered, is(equalTo("(1, 2)")));
+ }
+
+ @Test
+ void testRenderWithOddParams() {
+ Doc unbound = param("a")
+ .append(text(","))
+ .appendLineOrSpace(param("b"))
+ .bracket(2, lineOrEmpty(), text("("), text(")"));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ unbound.bind(
+ "a", text("1"),
+ "b");
+ });
+ }
+
+ @Test
+ void testRenderWithIllTypedParamKey() {
+ Doc unbound = param("a")
+ .append(text(","))
+ .appendLineOrSpace(param("b"))
+ .bracket(2, lineOrEmpty(), text("("), text(")"));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ unbound.bind(
+ "a", text("1"),
+ 1, text("2"));
+ });
+ }
+
+ @Test
+ void testRenderWithIllTypedParamValue() {
+ Doc unbound = param("a")
+ .append(text(","))
+ .appendLineOrSpace(param("b"))
+ .bracket(2, lineOrEmpty(), text("("), text(")"));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ unbound.bind(
+ "a", text("1"),
+ "b", "2");
+ });
+ }
+
/**
* This tests the {@code spread} operator of the original paper, implemented via
* the Haskell functions:
@@ -1016,8 +1107,8 @@ void groupTextEquivalentToIdentity(
@Property
void leftUnitLaw(
@ForAll @IntRange(min = 5, max = 200) int width,
- @ForAll("docs") Doc doc) {
- String appended = doc.append(empty()).render(width);
+ @ForAll("noParamDocs") Doc doc) {
+ String appended = doc.append(Doc.empty()).render(width);
String original = doc.render(width);
assertThat(appended, is(equalTo(original)));
}
@@ -1033,8 +1124,8 @@ void leftUnitLaw(
@Property
void rightUnitLaw(
@ForAll @IntRange(min = 5, max = 200) int width,
- @ForAll("docs") Doc doc) {
- String appended = empty().append(doc).render(width);
+ @ForAll("noParamDocs") Doc doc) {
+ String appended = Doc.empty().append(doc).render(width);
String original = doc.render(width);
assertThat(appended, is(equalTo(original)));
}
@@ -1050,9 +1141,9 @@ void rightUnitLaw(
@Property
void associativityLaw(
@ForAll @IntRange(min = 5, max = 200) int width,
- @ForAll("docs") Doc x,
- @ForAll("docs") Doc y,
- @ForAll("docs") Doc z) {
+ @ForAll("noParamDocs") Doc x,
+ @ForAll("noParamDocs") Doc y,
+ @ForAll("noParamDocs") Doc z) {
String leftAssociated = x.append(y).append(z).render(width);
String rightAssociated = x.append(y.append(z)).render(width);
assertThat(leftAssociated, is(equalTo(rightAssociated)));
@@ -1085,7 +1176,7 @@ void appendEquivalentToStringConcat(
@Property
void emptyEquivalentToEmptyText(@ForAll @IntRange(min = 5, max = 200) int width) {
String emptyText = text("").render(width);
- String emptyDoc = empty().render(width);
+ String emptyDoc = Doc.empty().render(width);
assertThat(emptyText, is(equalTo(emptyDoc)));
}
@@ -1101,7 +1192,7 @@ void nestedIndentEquivalentToSumIndent(
@ForAll @IntRange(min = 5, max = 200) int width,
@ForAll @IntRange(min = 0, max = 200) int i,
@ForAll @IntRange(min = 0, max = 200) int j,
- @ForAll("docs") Doc doc) {
+ @ForAll("noParamDocs") Doc doc) {
String sumIndent = doc.indent(i + j).render(width);
String nestedIndent = doc.indent(j).indent(i).render(width);
assertThat(sumIndent, is(equalTo(nestedIndent)));
@@ -1117,7 +1208,7 @@ void nestedIndentEquivalentToSumIndent(
@Property
void indentZeroEquivalentToNoIndent(
@ForAll @IntRange(min = 5, max = 200) int width,
- @ForAll("docs") Doc doc) {
+ @ForAll("noParamDocs") Doc doc) {
String zeroIndent = doc.indent(0).render(width);
String noIndent = doc.render(width);
assertThat(zeroIndent, is(equalTo(noIndent)));
@@ -1134,8 +1225,8 @@ void indentZeroEquivalentToNoIndent(
void indentDistributesOverAppend(
@ForAll @IntRange(min = 5, max = 200) int width,
@ForAll @IntRange(min = 0, max = 200) int indent,
- @ForAll("docs") Doc left,
- @ForAll("docs") Doc right) {
+ @ForAll("noParamDocs") Doc left,
+ @ForAll("noParamDocs") Doc right) {
String indentedAppend = left.append(right).indent(indent).render(width);
String appendedIndents = left.indent(indent).append(right.indent(indent)).render(width);
assertThat(indentedAppend, is(equalTo(appendedIndents)));
@@ -1152,8 +1243,8 @@ void indentDistributesOverAppend(
void emptyUnaffectedByIndent(
@ForAll @IntRange(min = 5, max = 200) int width,
@ForAll @IntRange(min = 0, max = 200) int indent) {
- String indentedEmpty = empty().indent(indent).render(width);
- String noIndentEmpty = empty().render(width);
+ String indentedEmpty = Doc.empty().indent(indent).render(width);
+ String noIndentEmpty = Doc.empty().render(width);
assertThat(indentedEmpty, is(equalTo(noIndentEmpty)));
}
@@ -1174,6 +1265,118 @@ void topLevelIndentEquivalentToNoIndent(
assertThat(topLevelIndent, is(equalTo(noIndent)));
}
+ @Property
+ void paramHasParams(@ForAll String paramName) {
+ Doc boundDoc = Doc.param(paramName);
+ assertThat(boundDoc.hasParams(), is(true));
+ }
+
+ @Property
+ void paramBindingEliminatesParam(
+ @ForAll String paramName,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ Doc boundDoc = Doc.param(paramName)
+ .bind(paramName, argDoc);
+ assertThat(boundDoc.hasParams(), is(false));
+ }
+
+ @Property
+ void paramBindingWrongNameDoesNothing(
+ @ForAll String paramName,
+ @ForAll String unrelatedName,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ Assume.that(!paramName.equals(unrelatedName));
+
+ Doc paramDoc = Doc.param(paramName);
+
+ Doc boundDoc = paramDoc.bind(unrelatedName, argDoc);
+
+ assertThat(boundDoc, is(sameInstance(paramDoc)));
+
+ assertThat(boundDoc.hasParams(), is(true));
+ }
+
+ @Property
+ void paramBindingWrongNameMapDoesNothing(
+ @ForAll String paramName,
+ @ForAll String unrelatedName1,
+ @ForAll String unrelatedName2,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ Assume.that(!paramName.equals(unrelatedName1));
+ Assume.that(!paramName.equals(unrelatedName2));
+ Assume.that(!unrelatedName1.equals(unrelatedName2));
+
+ Doc paramDoc = Doc.param(paramName);
+
+ Doc boundDoc = paramDoc.bind(
+ unrelatedName1, argDoc,
+ unrelatedName2, argDoc);
+
+ assertThat(boundDoc, is(sameInstance(paramDoc)));
+
+ assertThat(boundDoc.hasParams(), is(true));
+ }
+
+ @Property
+ void bindingTopLevelParamEquivalentToArgDoc(
+ @ForAll @IntRange(min = 5, max = 200) int width,
+ @ForAll String paramName,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ String renderedArg = argDoc.render(width);
+
+ String boundParam = Doc.param(paramName)
+ .bind(paramName, argDoc)
+ .render(width);
+
+ assertThat(boundParam, is(equalTo(renderedArg)));
+ }
+
+ @Property
+ void bindingTopLevelParamWithStringEquivalentToText(
+ @ForAll @IntRange(min = 5, max = 200) int width,
+ @ForAll String paramName,
+ @ForAll String paramValue
+ ) {
+ String renderedText = Doc.text(paramValue).render(width);
+
+ String boundParam = Doc.param(paramName)
+ .bind(paramName, paramValue)
+ .render(width);
+
+ assertThat(boundParam, is(equalTo(renderedText)));
+ }
+
+ @Property
+ void bindingDocWithoutParamsDoesNothing(
+ @ForAll("noParamDocs") Doc doc,
+ @ForAll String paramName,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ Doc boundDoc = doc.bind(paramName, argDoc);
+ assertThat(boundDoc, is(equalTo(doc)));
+ }
+
+ @Property
+ void paramIsEquivalentToInlining(
+ @ForAll @IntRange(min = 5, max = 200) int width,
+ @ForAll("unaryDocs") UnaryOperator unaryDoc,
+ @ForAll String paramName,
+ @ForAll("noParamDocs") Doc argDoc
+ ) {
+ String inlined = unaryDoc.apply(argDoc).render(width);
+
+ String parameterized = unaryDoc
+ .apply(param(paramName))
+ .bind(paramName, argDoc)
+ .render(width);
+
+ assertThat(parameterized, is(equalTo(inlined)));
+ }
+
@Test
void testEquals() {
Doc left = docs().sample();
@@ -1188,7 +1391,7 @@ void testEquals() {
// those prefab values must not be equal to each other
EqualsVerifier
.forClasses(
- Text.class, Append.class,
+ Text.class, Append.class, Param.class,
Alternatives.class, Indent.class,
LineOr.class, Escape.class, Styled.class)
.usingGetClass()
@@ -1203,7 +1406,7 @@ void testToString() {
Text.class, Append.class,
Alternatives.class, Indent.class,
LineOr.class, Empty.class, Escape.class,
- Reset.class, Styled.class)
+ Reset.class, Styled.class, Param.class)
.withPrefabValue(Doc.class, docs().sample())
.verify();
@@ -1213,6 +1416,74 @@ void testToString() {
.verify();
}
+ @Provide
+ Arbitrary paramDocs() {
+ return docs().filter(Doc::hasParams);
+ }
+
+ @Provide
+ Arbitrary noParamDocs() {
+ return docs().filter(doc -> !doc.hasParams());
+ }
+
+ @Provide
+ Arbitrary> unaryDocs() {
+ return Arbitraries.lazyOf(
+ () -> Arbitraries.of(Doc::group),
+ () -> Arbitraries.of(Doc::lineOr),
+ () -> Arbitraries.of(doc -> doc.indent(2)),
+ () -> Arbitraries.of(doc -> doc.bracket(2, lineOrEmpty(), text("["), text("]"))),
+ () -> noParamDocs().map(doc1 -> doc1::append),
+ () -> noParamDocs().map(doc1 -> doc2 -> doc2.append(doc1)),
+ () -> unaryDocs().tuple2().map(tuple -> doc -> tuple.get1().andThen(tuple.get2()).apply(doc))
+ );
+ }
+
+ @Provide
+ Arbitrary styles() {
+ return Arbitraries.lazyOf(
+ () -> colors().map(Styles::fg),
+ () -> colors().map(Styles::bg),
+ () -> Arbitraries.of(Styles.bold()),
+ () -> Arbitraries.of(Styles.faint()),
+ () -> Arbitraries.of(Styles.italic()),
+ () -> Arbitraries.of(Styles.underline()),
+ () -> Arbitraries.of(Styles.blink()),
+ () -> Arbitraries.of(Styles.inverse()),
+ () -> Arbitraries.of(Styles.strikethrough())
+ );
+ }
+
+ @Provide
+ Arbitrary colors() {
+ return Arbitraries.oneOf(
+ Arbitraries.create(Color::none),
+ Arbitraries.create(Color::black),
+ Arbitraries.create(Color::red),
+ Arbitraries.create(Color::green),
+ Arbitraries.create(Color::yellow),
+ Arbitraries.create(Color::blue),
+ Arbitraries.create(Color::magenta),
+ Arbitraries.create(Color::cyan),
+ Arbitraries.create(Color::white),
+
+ Arbitraries.create(Color::brightBlack),
+ Arbitraries.create(Color::brightRed),
+ Arbitraries.create(Color::brightGreen),
+ Arbitraries.create(Color::brightYellow),
+ Arbitraries.create(Color::brightBlue),
+ Arbitraries.create(Color::brightMagenta),
+ Arbitraries.create(Color::brightCyan),
+ Arbitraries.create(Color::brightWhite),
+
+ Arbitraries.integers().between(0, 255)
+ .map(Color::xterm),
+
+ Arbitraries.integers().between(0, 255)
+ .tuple3().map(t -> Color.rgb(t.get1(), t.get2(), t.get3()))
+ );
+ }
+
@Provide
Arbitrary docs() {
return Arbitraries.lazyOf(
@@ -1236,7 +1507,14 @@ Arbitrary docs() {
// Bracketing
() -> docs().map(doc -> doc.bracket(2, Doc.lineOrEmpty(), Doc.text("["), Doc.text("]"))),
// Alternatives
- () -> docs().map(Doc::group));
+ () -> docs().map(Doc::group),
+ // Param
+ () -> Arbitraries.strings().map(Doc::param),
+ // Styled
+ () -> styles().array(Styles.StylesOperator[].class).flatMap(styles -> {
+ return docs().map(doc -> doc.styled(styles));
+ })
+ );
}
String sgrCode(int code) {
diff --git a/src/test/java/com/opencastsoftware/prettier4j/RenderOptionsTest.java b/src/test/java/com/opencastsoftware/prettier4j/RenderOptionsTest.java
new file mode 100644
index 0000000..3d70bbf
--- /dev/null
+++ b/src/test/java/com/opencastsoftware/prettier4j/RenderOptionsTest.java
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: © 2024 Opencast Software Europe Ltd
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.opencastsoftware.prettier4j;
+
+import com.jparams.verifier.tostring.ToStringVerifier;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.jupiter.api.Test;
+
+public class RenderOptionsTest {
+ @Test
+ void testEquals() {
+ EqualsVerifier.forClass(RenderOptions.class).usingGetClass().verify();
+ }
+
+ @Test
+ void testToString() {
+ ToStringVerifier.forClass(RenderOptions.class).verify();
+ }
+}