Skip to main content

module lang::box::util::Box2Text

rascal-Not specified

Two-dimensional text layout algorithm

Usage​

import lang::box::util::Box2Text;

Dependencies​

import util::Math;
import List;
import String;
import lang::box::\syntax::Box;
import IO;

Description​

The input to Box2Text is a hierarchy of "Boxes" represented by the ((lang::box::\syntax::Box)) algebraic data-type. These boxes put hard and soft relative positioning constraints on the embedded text fragments, and there is the global soft constraints of the width of the screen (or the paper).

This implementation is a port from ASF+SDF to Rascal. The ASF+SDF implementation was published as "From Box to Tex:An algebraic approach to the construction of documentation tools" by Mark van den Brand and Eelco Visser (June 30, 1994). The original Box concept was introduced by Joel Coutaz as this technical report: "The Box, A Layout Abstraction for User Interface Toolkits" (1984) Pittsburgh, PA: Carnegie Mellon University.

The main function format maps a Box tree to a str:

  • To obtain Box terms people typically transform ASTs or Parse Trees to Box using pattern matching in Rascal.
  • Options encode global default options for constraint parameters that only override local parameters if they were elided.

Examples​

This demonstrates the semantics of the main hard constraints:

  • H for horizontal;
  • V for vertical;
  • I for indentation.
rascal>import lang::box::util::Box2Text;
ok
rascal>import lang::box::\syntax::Box;
ok
rascal>format(H(L("A"), L("B"), L("C"), hs=2))
str: "A B C\n"
───
A B C

───
rascal>format(H(L("A"), L("B"), L("C"), hs=1))
str: "A B C\n"
───
A B C

───
rascal>format(H(L("A"), L("B"), L("C"), hs=0))
str: "ABC\n"
───
ABC

───
rascal>format(V(L("A"), L("B"), L("C"), vs=2))
str: "A\n\n\nB\n\n\nC\n"
───
A


B


C

───
rascal>format(V(L("A"), L("B"), L("C"), vs=1))
str: "A\n\nB\n\nC\n"
───
A

B

C

───
rascal>format(V(L("A"), L("B"), L("C"), vs=0))
str: "A\nB\nC\n"
───
A
B
C

───
rascal>format(H(L("A"), V(L("B"), L("C"))))
str: "A B\n C\n"
───
A B
C

───
rascal>format(H(L("A"), I(L("B")), L("C")))
str: "A B C\n"
───
A B C

───
rascal>format(H(L("A"), V(L("B"), H(L("C"), L("D")))))
str: "A B\n C D\n"
───
A B
C D

───

The "soft" constraints change their behavior based on available horizontal room:

rascal>format(HV([L("W<i>") | i <- [0..10]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9

───
rascal>format(HV([L("W<i>") | i <- [0..20]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19

───
rascal>format(HV([L("W<i>") | i <- [0..40]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31\nW32 W33 W34 W35 W36 W37 W38 W39\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31
W32 W33 W34 W35 W36 W37 W38 W39

───
rascal>format(HV([L("W<i>") | i <- [0..80]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31\nW32 W33 W34 W35 W36 W37 W38 W39 W40 W41 W42 W43 W44 W45 W46 W47 W48 W49 W50 W51 W52 W53 W54 W55 W56 W57 W58 W59 W60\nW61 W62 W63 W64 W65 W66 W67 W68 W69 W70 W71 W72 W73 W74 W75 W76 W77 W78 W79\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31
W32 W33 W34 W35 W36 W37 W38 W39 W40 W41 W42 W43 W44 W45 W46 W47 W48 W49 W50 W51 W52 W53 W54 W55 W56 W57 W58 W59 W60
W61 W62 W63 W64 W65 W66 W67 W68 W69 W70 W71 W72 W73 W74 W75 W76 W77 W78 W79

───
rascal>format(HV([L("W<i>") | i <- [0..100]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31\nW32 W33 W34 W35 W36 W37 W38 W39 W40 W41 W42 W43 W44 W45 W46 W47 W48 W49 W50 W51 W52 W53 W54 W55 W56 W57 W58 W59 W60\nW61 W62 W63 W64 W65 W66 W67 W68 W69 W70 W71 W72 W73 W74 W75 W76 W77 W78 W79 W80 W81 W82 W83 W84 W85 W86 W87 W88 W89\nW90 W91 W92 W93 W94 W95 W96 W97 W98 W99\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29 W30 W31
W32 W33 W34 W35 W36 W37 W38 W39 W40 W41 W42 W43 W44 W45 W46 W47 W48 W49 W50 W51 W52 W53 W54 W55 W56 W57 W58 W59 W60
W61 W62 W63 W64 W65 W66 W67 W68 W69 W70 W71 W72 W73 W74 W75 W76 W77 W78 W79 W80 W81 W82 W83 W84 W85 W86 W87 W88 W89
W90 W91 W92 W93 W94 W95 W96 W97 W98 W99

───
rascal>format(HOV([L("W<i>") | i <- [0..10]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9

───
rascal>format(HOV([L("W<i>") | i <- [0..20]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19

───
rascal>format(HOV([L("W<i>") | i <- [0..30]]));
str: "W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29\n"
───
W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29

───

By cleverly combining constraints, a specifically desired behavior is easy to achieve:

rascal>format(H(L("if"), H(L("("), L("true"), L(")"), hs=0), HOV(L("doSomething"))))
str: "if (true) doSomething\n"
───
if (true) doSomething

───
rascal>format(H(L("if"), H(L("("), L("true"), L(")"), hs=0), HOV([L("W<i>") | i <- [0..30]])))
str: "if (true) W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29\n"
───
if (true) W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29

───
rascal>format(H(L("if"), H(L("("), L("true"), L(")"), hs=0), HV([L("W<i>") | i <- [0..30]])))
str: "if (true) W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29\n"
───
if (true) W0 W1 W2 W3 W4 W5 W6 W7 W8 W9 W10 W11 W12 W13 W14 W15 W16 W17 W18 W19 W20 W21 W22 W23 W24 W25 W26 W27 W28 W29

───

Pitfalls​

  • Box2text does not have highlighting features anymore; you can use Highlight for this instead.

data FormattingOptions​

formatting options for ((Box2Text))

data FormattingOptions (
int maxWidth=120,
int wrapAfter=90,
int tabSize = 4,
bool insertSpaces = true,
bool trimTrailingWhitespace = true,
bool insertFinalNewline = true,
bool trimFinalNewlines = true
)
= formattingOptions()
;
  • maxWidth is the constraint that makes HV and HOV boxes switch to vertical mode
  • wrapAfter is the lowerbound that makes HV and HOV stay horizontal
  • tabSize is the default indentation used when an I box does not have an explicit is parameter
  • insertSpaces, when set to true it prefers spaces over tabs, when set to false we use tabs for indentation (see tabSize)
  • trimTrailingWhitespace when true the formatter can not leave spaces or tabs after the last non-whitespace character, when false it does not matter.
  • insertFinalNewline, insert a newline character at the end of the file if one does not exist.
  • trimFinalNewlines, trim all newlines after the final newline at the end of the file.

Note that there may be more FormattingOptions due to other elements of a formatting pipeline, such as Layout Diff.

data Alignment​

Specialized alignments for the final column to implement trimTrailingWhitespace

data Alignment  
= fl()
| fc()
;

function format​

Converts boxes into a string by finding an "optimal" two-dimensional layout

str format(Box b, FormattingOptions opts = formattingOptions())
  • This algorithm never changes the left-to-right order of the Boxes constituents, such that syntactical correctness is maintained
  • This algorithm tries to never over-run the maxWidth parameter, but if it must to maintain text order, and the specified nesting of boxes, it will anyway. For example, if a table column doesn't fit it will still be printed. We say maxWidth is a soft constraint.
  • Separator options like i, h and v options are hard constraints, they may lead to overriding maxWidth.
  • H, V and I boxes represent hard constraints too.
  • HV and HOV are the soft constraints that allow for better solutions, so use them where you can to allow for flexible layout that can handle deeply nested expressions and statements.

function finalNewlineOptions​

str finalNewlineOptions(str lines, bool insertFinalNewline, bool trimFinalNewlines)

alias Text​

Box2text uses list[str] as intermediate representation of the output during formatting

list[str]

Benefits​

  • Helps with fast concatenation
  • Allows for measuring (max) width and height of intermediate results very quickly

Pitfalls​

  • Because of this representation, box2text does not recognize that unprintable characters have width 0. So, ANSI escape codes, and characters like \r and \n in L boxes will break the accuracy of the algorithm.

function box2text​

Converts boxes into list of lines (Unicode)

Text box2text(Box b, FormattingOptions opts = formattingOptions())

data Options​

Configuration options for a single formatting run.

data Options  
= options(
int hs = 1,
int vs = 0,
int is = 4,
int maxWidth = 80,
int wrapAfter = 70,
bool trimTrailingWhitespace = true,
bool insertSpaces = true
)
;

This is used during the algorithm, not for external usage.

  • hs is the current separation between every horizontal element in H, HV and HOV boxes
  • vs is the current separation between vertical elements in V, HV and HOV boxes
  • is is the default (additional) indentation for indented boxes
  • maxWidth is the number of columns (characters) of a single line on screen or on paper
  • wrapAfter is the threshold criterion for line fullness, to go to the next line in a HV box and to switching between horizontal and vertical for HOV boxes.

function u​

Quickly splice in any nested U boxes, and empty H, V, HV, I or HOV boxes

list[Box] u(list[Box] boxes)

function isDegenerate​

Empty H, V, HOV, HV, I boxes should not lead to accidental extra separators in their context

bool isDegenerate(Box b)

function vv​

simple vertical concatenation (every list element is a line)

Text vv(Text a, Text b)

function blank​

Create a string of spaces just as wide as the parameter a

str blank(str a, Options opts)

function wd​

Computes a white line with the length of the last line of a

Text wd([], Options _)

Text wd([*_, str x], Options opts)

function twidth​

Computes the maximum width of text t

int twidth([])

default int twidth(Text t)

function hwidth​

Computes the length of the last line of t

int hwidth([])

int hwidth([*_, str last])

function bar​

Prepends str a before text b, all lines of b will be shifted

Text bar(str a, [], Options _)

Text bar(str a, [str bh, *str bt], Options opts)

function hskip​

Produce text consisting of a white line of length n

Text hskip(int n, Options opts)

function vskip​

Produces text consisting of n white lines at length 0

Text vskip(int n)

function prepend​

Prepend Every line in b with a

Text prepend(str a, Text b)

function hh​

Implements horizontal concatenation, also for multiple lines

Text hh([], Text b, Options _)

Text hh(Text a, [], Options _)

Text hh([a], Text b, Options opts)

default Text hh(Text a, Text b, Options opts)

function lhh​

Horizontal concatenation, but if the left text is empty return nothing.

Text lhh([], Text _, Options _)

default Text lhh(a, b, Options opts)

function rhh​

Horizontal concatenation, but if the right text is empty return nothing.

Text rhh(Text _, [], Options _)

Text rhh(Text a, Text b, Options opts)

function rvv​

Vertical concatenation, but if the right text is empty return nothing.

Text rvv(Text _, [])

default Text rvv(Text a, Text b)

function LL​

Text LL(str s)

function HH​

Text HH([], Box _, Options _opts, int _m)

Text HH(list[Box] b:[_, *_], Box _, Options opts, int m)

function GG​

Text GG(list[Box] boxes, Box c, Options opts, int m, int gs, Box op, bool backwards)

function groupBy​

list[Box] groupBy(list[Box] boxes, int gs, Box op, false)

function groupBy​

simulates grouping as-if done from the back, by starting to peel off the rest instead of grouping the rest at the end

list[Box] groupBy(list[Box] boxes, int gs, Box op, true)

list[Box] groupBy([], int _gs, Box _op)

list[Box] groupBy(list[Box] boxes:[Box _, *_], int gs, Box op)

function VV​

Text VV([], Box _c, Options _opts, int _m)

Text VV(list[Box] b:[_, *_], Box c, Options opts, int m)

function II​

Text II([], Box _c, Options _opts, int _m)

Text II(list[Box] b:[_, *_] , c:H_(list[Box] _), Options opts, int m)

Text II(list[Box] b:[Box _, *Box _], c:V_(list[Box] _), Options opts, int m)

function WDWD​

Text WDWD([], Box _c , Options _opts, int _m)

Text WDWD([Box head, *Box tail], Box c , Options opts, int m)

function ifHOV​

Text ifHOV([], Box b,  Box c, Options opts, int m)

Text ifHOV(Text t:[str head], Box b, Box c, Options opts, int m)

Text ifHOV(Text t:[str head, str _, *str_], Box b, Box c, Options opts, int m)

function HOVHOV​

Text HOVHOV(list[Box] b, Box c, Options opts, int m)

function HVHV​

Text HVHV(Text T, int s, Text a, Box A, list[Box] B, Options opts, int m)

Text HVHV(Text T, int _s, [], Options _opts, int _m, Box _c)

Text HVHV(Text T, int s, [Box head, *Box tail], Options opts, int m, Box c)

Text HVHV([], Box _, Options opts, int m)

Text HVHV(list[Box] b:[Box head], Box _, Options opts, int m)

Text HVHV(list[Box] b:[Box head, Box next, *Box tail], Box _, Options opts, int m)

function continueWith​

Text continueWith(Box b:L(str s)         , Box c, Options opts, int m)

Text continueWith(Box b:H_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:V_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:I_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:WD_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:HOV_(list[Box] bl), Box c, Options opts, int m)

Text continueWith(Box b:HV_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:SPACE(int n) , Box c, Options opts, int m)

Text continueWith(Box b:U_(list[Box] bl) , Box c, Options opts, int m)

Text continueWith(Box b:G_(list[Box] bl), Box c, Options opts, int m)

Text continueWith(Box b:A_(list[Row] rows), Box c, Options opts, int m)

Text continueWith(Box b:AG_(list[Box] boxes), Box c, Options opts, int m)

function \continue​

Option inheritance layer; then continue with the next box.

Text \continue(Box b, Box c, Options opts, int m)

The next box is either configured by itself. Options are transferred from the box to the opts parameter for easy passing on to recursive calls.

data Box​

This is to store the result of the first pass of the algorithm over all the cells in an array/table

data Box (int width=0, int height=1)

function boxSize​

Completely layout a box and then measure its width and height, and annotate the result into the Box

Box boxSize(Box b, Box c, Options opts, int m)

function RR​

list[list[Box]] RR(list[Row] bl, Box c, Options opts, int m)

function Acolumns​

Compute the maximum number of columns of the rows in a table

int Acolumns(list[Row] rows)

function Awidth​

Compute the maximum cell width for each column in an array

list[int] Awidth(list[list[Box]] rows)

function AcompleteRows​

Adds empty cells to every row until every row has the same amount of columns.

list[Row] AcompleteRows(list[Row] rows, int columns=Acolumns(rows), Box rs=NULL())

function align​

Helper function for aligning Text inside an array cell

Box align(l(), Box cell, int maxWidth)

function align​

Helper function for aligning Text inside an array cell

Box align(r(), Box cell, int maxWidth)

Box align(c(), Box cell, int maxWidth)

Box align(fl(), Box cell, int maxWidth)

Box align(fc(), Box cell, int maxWidth)

function AA​

Text AA(list[Row] table, Box c, list[Alignment] alignments, Box rs, Options opts, int m)

function AAG​

Text AAG([], int _gs, list[Alignment] _columns, Box _rs, Box _c, Options _opts, int _m)

Text AAG(list[Box] boxes:[Box _, *_], int gs, list[Alignment] columns, Box rs, Box c, Options opts, int m)

function groupRows​

list[Row] groupRows([], int _gs)

list[Row] groupRows(list[Box] boxes:[Box _, *_], int gs)

function AcompleteAlignments​

Cuts off and extends the alignment spec to the width of the table

list[Alignment] AcompleteAlignments(list[Alignment] alignments, int maxColumns)
  • if too few columns are specified: l()'s are added accordingly
  • if too many columns are specified: they are cut off from the right

function AfinalColumnSpecials​

Translate l() and c() to fl() and fc() for the final columns to help implement trimTrailingWhitespace

list[Alignment] AfinalColumnSpecials([*Alignment pre, l()])

list[Alignment] AfinalColumnSpecials([*Alignment pre, c()])

default list[Alignment] AfinalColumnSpecials(list[Alignment] as)

function noWidthOverflow​

Check soft limit for HV and HOV boxes

bool noWidthOverflow(list[Box] hv, Options opts)

function applyHVconstraints​

Changes all HV boxes that do fit horizontally into hard H boxes.

Box applyHVconstraints(Box b, Options opts)

function applyHOVconstraints​

Changes all HOV boxes that do fit horizontally into hard H boxes, and the others into hard V boxes.

Box applyHOVconstraints(Box b, Options opts)

function box2data​

Workhorse, that first applies hard HV and HOV limits and then starts the general algorithm

Text box2data(Box b, Options opts)

Tests

test horizontalPlacement2​

test bool horizontalPlacement2()
= format(H(L("A"), L("B"), L("C"), hs=2))
== "A B C
'";

test horizontalPlacement3​

test bool horizontalPlacement3()
= format(H(L("A"), L("B"), L("C"), hs=3))
== "A B C
'";

test horizontalIndentIsNoop1​

test bool horizontalIndentIsNoop1() 
= format(H(L("A"), I(L("B"))))
== "A B
'";

test horizontalIndentIsNoop2​

test bool horizontalIndentIsNoop2() 
= format(HV(L("A"), I(L("B"))))
== "A B
'";

test horizontalIndentIsNoop3​

test bool horizontalIndentIsNoop3() 
= format(HOV(L("A"), I(L("B"))))
== "A B
'";

test emptyBoxesNoExtraSpacing1​

test bool emptyBoxesNoExtraSpacing1()
= format(H(L("A"), H(), L("B")))
== "A B
'";

test emptyBoxesNoExtraSpacing2​

test bool emptyBoxesNoExtraSpacing2()
= format(H(L("A"), V(), L("B")))
== "A B
'";

test emptyBoxesNoExtraSpacing3​

test bool emptyBoxesNoExtraSpacing3()
= format(H(L("A"), I(), L("B")))
== "A B
'";

test emptyBoxesNoExtraSpacing4​

test bool emptyBoxesNoExtraSpacing4()
= format(V(L("A"), H(), L("B")))
== "A
'B
'";

test emptyBoxesNoExtraSpacing5​

test bool emptyBoxesNoExtraSpacing5()
= format(V(L("A"), V(), L("B")))
== "A
'B
'";

test verticalPlacement0​

test bool verticalPlacement0()
= format(V(L("A"), L("B"), L("C"), vs=0))
== "A
'B
'C
'";

test verticalPlacement1​

test bool verticalPlacement1()
= format(V(L("A"), L("B"), L("C"), vs=1))
== "A
'
'B
'
'C
'";

test verticalIndentation2​

test bool verticalIndentation2()
= format(V(L("A"), I(L("B")), L("C")))
== "A
' B
'C
'";

test blockIndent​

test bool blockIndent()
= format(V(L("A"), I(V(L("B"), L("C"))), L("D")))
== "A
' B
' C
'D
'";

test wrappingIgnoreIndent​

test bool wrappingIgnoreIndent()
= format(HV(L("A"), I(L("B")), L("C"), hs=0), opts=formattingOptions(maxWidth=2, wrapAfter=2))
== "AB
'C
'";

test wrappingWithIndent​

test bool wrappingWithIndent()
= format(HV(L("A"), I(L("B")), I(L("C")), hs=0),opts=formattingOptions( maxWidth=2, wrapAfter=2))
== "AB
' C
'";

test multiBoxIndentIsVertical​

test bool multiBoxIndentIsVertical()
= format(I(L("A"), L("B")))
== " A
' B
'";

test flipping1NoIndent​

test bool flipping1NoIndent()
= format(HOV(L("A"), L("B"), L("C"), hs=0, vs=0), opts=formattingOptions(maxWidth=2, wrapAfter=2))
== "A
'B
'C
'";

test horizontalOfOneVertical​

test bool horizontalOfOneVertical()
= format(H(L("A"), V(L("B"), L("C"))))
== "A B
' C
'";

test stairCase​

test bool stairCase()
= format(H(L("A"), V(L("B"), H(L("C"), V(L("D"), H(L("E"), L("F")))))))
== "A B
' C D
' E F
'";

test simpleTable​

test bool simpleTable() 
= format(A(R([L("1"),L("2"),L("3")]),R([L("4"), L("5"), L("6")]),R([L("7"), L("8"), L("9")])))
== "1 2 3
'4 5 6
'7 8 9
'";

test simpleAlignedTable​

test bool simpleAlignedTable() 
= format(A(R([L("1"),L("2"),L("3")]),R([L("44"), L("55"), L("66")]),R([L("777"), L("888"), L("999")]),
columns=[l(),c(),r()]))
== "1 2 3
'44 55 66
'777 888 999
'";

test simpleAlignedTableDifferentAlignment​

test bool simpleAlignedTableDifferentAlignment() 
= format(A(R([L("1"),L("2"),L("3")]),R([L("44"), L("55"), L("66")]),R([L("777"), L("888"), L("999")]),
columns=[r(),c(),l()]))
== " 1 2 3
' 44 55 66
'777 888 999
'";

test WDtest​

test bool WDtest() {
L1 = H(L("aap") , hs=0);
L2 = H(WD(L1), L("noot"), hs=0);
L3 = H(WD(L2), L("mies"), hs=0);

return format(V(L1, L2, L3))
== "aap
' noot
' mies
'";
}

test groupByTest​

test bool groupByTest() {
lst = [L("<i>") | i <- [0..10]];
g1 = G(lst, op=H(), gs=3);
lst2 = [H(L("<i>"), L("<i+1>"), L("<i+2>")) | i <- [0,3..7]] + [H(L("9"))];

return format(V(g1)) == format(V(lst2));
}

test groupByBackwardsTest​

test bool groupByBackwardsTest() {
lst = [L("<i>") | i <- [0..10]];
g1 = G(lst, op=H(), gs=3, backwards=true);
lst2 = [H(L("0"))] + [H(L("<i>"), L("<i+1>"), L("<i+2>")) | i <- [1, 4..10]];

return format(V([g1])) == format(V(lst2));
}

test noDegenerateHVSeparators1​

test bool noDegenerateHVSeparators1()
= format(HV(L("a"),V(),L("b")))
== "a b
'";

test noDegenerateHVSeparators2​

test bool noDegenerateHVSeparators2()
= format(HV(L("a"),V(),L("b")), opts=formattingOptions(maxWidth=1, wrapAfter=1))
== "a
'b
'";

test noDegenerateHOVSeparators1​

test bool noDegenerateHOVSeparators1()
= format(HOV(L("a"),V(),L("b")))
== "a b
'";

test noDegenerateHOVSeparators2​

test bool noDegenerateHOVSeparators2()
= format(HOV(L("a"),V(),L("b")), opts=formattingOptions(maxWidth=1, wrapAfter=1))
== "a
'b
'";