Krow

A small DSL for generating tables in ASCII or HTML formats

GitHub release (latest by date) Maven Central Kotlin Version

Overview

val table = krow {
    cell("col1", "row1") { content = "1-1" }
    cell("col1", "row2") { content = "1-2" }

    cell("col2", "row1") { content = "2-1" }
    cell("col2", "row2") { content = "2-2" }

    cell("col3", "row1") { content = "3-1" }
    cell("col3", "row2") { content = "3-2" }

    table {
        wrapTextAt = 30
        horizontalAlignment = HorizontalAlignment.CENTER
        verticalAlignment = VerticalAlignment.TOP
    }
}

Installation

repositories {
    mavenCentral()
}

// for plain JVM or Android projects
dependencies {
    implementation("io.github.copper-leaf:krow-core:1.0.0")
}

// for multiplatform projects
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.github.copper-leaf:krow-core:1.0.0")
            }
        }
    }
}

Targets

Krow renders both ASCII and HTML as plain Strings, with no platform-specific formatting. Krow publishes artifacts for the following Kotlin targets.

Platform
Android
JVM
iOS
JS

Usage

Krow is a DSL for the layout and rendering of HTML-like tables. It supports table cells that can span both rows and columns, as well as several border styles to tweak the presentation of ASCII tables.

DSL

Header Columns

To build a table with 3 columns, such as:

val expected = """
    ┌──────┬──────────┬──────────┬──────────┐
    │      │ column 1 │ column 2 │ column 3 │
    ├──────┼──────────┼──────────┴──────────┤
    │ row1 │ 1        │ 2                   │
    └──────┴──────────┴─────────────────────┘
""".trimIndent()

all of the following styles are equivalent:

krow {
    header {
        row {
            column("column 1")
            column("column 2")
            column("column 3")
        }
    }
    body {
        row("row1") {
            cell("1")
            cell("2") { colSpan = 2 }
        }
    }
}
krow {
    header {
        column("column 1")
        column("column 2")
        column("column 3")
    }
    body {
        row("row1") {
            cell("1")
            cell("2") { colSpan = 2 }
        }
    }
}
krow {
    header {
        row {
            columns("column 1", "column 2", "column 3")
        }
    }
    body {
        row("row1") {
            cell("1")
            cell("2") { colSpan = 2 }
        }
    }
}
krow {
    header {
        columns("column 1", "column 2", "column 3")
    }
    body {
        row("row1") {
            cell("1")
            cell("2") { colSpan = 2 }
        }
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    body {
        row("row1") {
            cell("1")
            cell("2") { colSpan = 2 }
        }
    }
}

Body Rows

To build a table with 2 columns, such as:

val expected = """
    ┌──────┬──────────┬──────────┬──────────┐
    │      │ column 1 │ column 2 │ column 3 │
    ├──────┼──────────┼──────────┼──────────┤
    │ row1 │ 1        │ 2        │ 3        │
    ├──────┼──────────┼──────────┼──────────┤
    │ row2 │ a        │ b        │ c        │
    └──────┴──────────┴──────────┴──────────┘
""".trimIndent()

all of the following styles are equivalent:

krow {
    headerColumns("column 1", "column 2", "column 3")
    body {
        row("row1") {
            cell("1")
            cell("2")
            cell("3")
        }
        row("row2") {
            cell("a")
            cell("b")
            cell("c")
        }
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    body {
        row("row1") {
            cells("1", "2", "3")
        }
        row("row2") {
            cells("a", "b", "c")
        }
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    body {
        rows(
            "row1" to listOf("1", "2", "3"),
            "row2" to listOf("a", "b", "c")
        )
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow("row1") {
        cell("1")
        cell("2")
        cell("3")
    }
    bodyRow("row2") {
        cell("a")
        cell("b")
        cell("c")
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow("row1") {
        cells("1", "2", "3")
    }
    bodyRow("row2") {
        cells("a", "b", "c")
    }
}
krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRows(
        "row1" to listOf("1", "2", "3"),
        "row2" to listOf("a", "b", "c")
    )
}

Dynamic Layout

The above snippets all assume a statically-defined layout of columns and rows, but the core layout engine of Krow allows you to build tables much more dynamically as well. Rather than manually defining columns and the names of rows, you can instead just define the content of the table, and it will expand and adapt to the content.

The following are some examples of dynamically-laid out tables, and how they get rendered.

Rows

By omitting a row name for each body row, the table will generate the row name as the row index.

val table = krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow {
        cells("1", "2", "3")
    }
    bodyRow {
        cells("a", "b", "c")
    }
}
val expected = """
        ┌───┬──────────┬──────────┬──────────┐
        │   │ column 1 │ column 2 │ column 3 │
        ├───┼──────────┼──────────┼──────────┤
        │ 1 │ 1        │ 2        │ 3        │
        ├───┼──────────┼──────────┼──────────┤
        │ 2 │ a        │ b        │ c        │
        └───┴──────────┴──────────┴──────────┘
""".trimIndent()

Columns

Body cells align themselves to columns based on the space available in the row. If a cell is placed at an index that was not specified as a column, that column will be added at the end of the table, with the index of the column used as its column name.

val table = krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow {
        cells("1", "2", "3", "4", "5")
    }
    bodyRow {
        cells("a", "b", "c", "d", "e")
    }
}
val expected = """
    ┌───┬──────────┬──────────┬──────────┬───┬───┐
    │   │ column 1 │ column 2 │ column 3 │ 4 │ 5 │
    ├───┼──────────┼──────────┼──────────┼───┼───┤
    │ 1 │ 1        │ 2        │ 3        │ 4 │ 5 │
    ├───┼──────────┼──────────┼──────────┼───┼───┤
    │ 2 │ a        │ b        │ c        │ d │ e │
    └───┴──────────┴──────────┴──────────┴───┴───┘
""".trimIndent()

This can be expanded out such that manually specifying columns is entirely optional.

val table = krow {
    bodyRow {
        cells("1", "2", "3")
    }
    bodyRow {
        cells("a", "b", "c")
    }
}
val expected = """
        ┌───┬───┬───┬───┐
        │   │ 1 │ 2 │ 3 │
        ├───┼───┼───┼───┤
        │ 1 │ 1 │ 2 │ 3 │
        ├───┼───┼───┼───┤
        │ 2 │ a │ b │ c │
        └───┴───┴───┴───┘
""".trimIndent()

Cells

If a previous cell in the row has a row span, a cell will be placed in the next available column within that row, adding columns as necessary.

val table = krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow {
        cell("1") { colSpan = 2 }
        cell("2")
        cell("3") { colSpan = 2 }
    }
    bodyRow {
        cells("a", "b", "c", "d", "e")
    }
}
val expected = """
    ┌───┬──────────┬──────────┬──────────┬───┬───┐
    │   │ column 1 │ column 2 │ column 3 │ 4 │ 5 │
    ├───┼──────────┴──────────┼──────────┼───┴───┤
    │ 1 │ 1                   │ 2        │ 3     │
    ├───┼──────────┬──────────┼──────────┼───┬───┤
    │ 2 │ a        │ b        │ c        │ d │ e │
    └───┴──────────┴──────────┴──────────┴───┴───┘
""".trimIndent()

Building with Coordinates

Krow also includes a cellAt function which places and configures a cell at the specified row/column coordinates, rather than building it in-order with the normal row/column builders. As normal, the rows/columns will expand as-necessary to fully accept each cell as it is specified, with row and column spans.

val table = krow {
    headerColumns("column 1", "column 2", "column 3")
    cellAt("row1", "column 1") {
        colSpan = 2
        content = "1"
    }
    cellAt("row1", "column 3") {
        content = "2"
    }
    cellAt("row1", "column 4") {
        colSpan = 2
        content = "3"
    }

    cellAt("row2", "column 1") {
        content = "a"
    }
    cellAt("row2", "column 2") {
        content = "b"
    }
    cellAt("row2", "column 3") {
        content = "c"
    }
    cellAt("row2", "column 4") {
        content = "d"
    }
    cellAt("row2", "5") {
        content = "e"
    }
}
val expected = """
    ┌──────┬──────────┬──────────┬──────────┬──────────┬───┐
    │      │ column 1 │ column 2 │ column 3 │ column 4 │ 5 │
    ├──────┼──────────┴──────────┼──────────┼──────────┴───┤
    │ row1 │ 1                   │ 2        │ 3            │
    ├──────┼──────────┬──────────┼──────────┼──────────┬───┤
    │ row2 │ a        │ b        │ c        │ d        │ e │
    └──────┴──────────┴──────────┴──────────┴──────────┴───┘
""".trimIndent()

If a cell already exists at these coordinates, it will allow you to customize the attributes of that cell, rather than attempting to create a new one. This makes it also useful for building the table structure with the row/column builders, then tweaking some of the content or styling of individual cells afterward.

val table = krow {
    headerColumns("column 1", "column 2", "column 3")
    bodyRow {
        cells("1", "2", "3")
    }
    bodyRow {
        cells("a", "b", "c")
    }

    cellAt("2", "column 2") { content = "overridden with cellAt" }
}
val expected = """
    ┌───┬──────────┬────────────────────────┬──────────┐
    │   │ column 1 │ column 2               │ column 3 │
    ├───┼──────────┼────────────────────────┼──────────┤
    │ 1 │ 1        │ 2                      │ 3        │
    ├───┼──────────┼────────────────────────┼──────────┤
    │ 2 │ a        │ overridden with cellAt │ c        │
    └───┴──────────┴────────────────────────┴──────────┘
""".trimIndent()

Be careful when using this layout DSL that the cell configurations do not overlap, which will throw an exception. It is also an error to attempt to change the rowSpan or colSpan after the cell has been created.

Rendering

Krow tables can be rendered either as ASCII tables, using a custom layout/rendering algorithm, or as HTML text to be displayed in a browser. So far, all examples have been shown using the ASCII renderer.

ASCII

Tables are rendered to ASCII with the AsciiTableFormatter. You can customize row widths and choose whether to display the header row or leading column.

val borders = DoubleBorder()
val table = krow {
    // ...
}
AsciiTableFormatter(borders).print(table)
krow {
    header {
        row {
            column("column 1")
            column("column 2") { width = 16 }
            column("column 3")
        }
    }
    bodyRow("row1") { cells("1", "2", "3") }
    bodyRow("row2") { cells("a", "b", "c") }

    cellAt("row1", "column 2") {
        content = "This is some long string which should wrap at the " +
            "appropriate width, which will also chop down reallyreallyreallyreallyreallylong " +
            "words if needed"
    }
}
┌──────┬──────────┬────────────────┬──────────┐
│      │ column 1 │ column 2       │ column 3 │
├──────┼──────────┼────────────────┼──────────┤
│ row1 │ 1        │ This is some   │ 3        │
│      │          │ long string    │          │
│      │          │ which should   │          │
│      │          │ wrap at the    │          │
│      │          │ appropriate    │          │
│      │          │ width, which   │          │
│      │          │ will also chop │          │
│      │          │ down reallyre- │          │
│      │          │ allyreallyrea- │          │
│      │          │ llyreallylong  │          │
│      │          │ words if       │          │
│      │          │ needed         │          │
├──────┼──────────┼────────────────┼──────────┤
│ row2 │ a        │ b              │ c        │
└──────┴──────────┴────────────────┴──────────┘
krow {
    includeHeaderRow = false
    header {
        row {
            column("column 1")
            column("column 2") { width = 16 }
            column("column 3")
        }
    }
    bodyRow("row1") { cells("1", "2", "3") }
    bodyRow("row2") { cells("a", "b", "c") }

    cellAt("row1", "column 2") {
        content = "This is some long string which should wrap at the " +
            "appropriate width, which will also chop down reallyreallyreallyreallyreallylong " +
            "words if needed"
    }
}
┌──────┬───┬────────────────┬───┐
│ row1 │ 1 │ This is some   │ 3 │
│      │   │ long string    │   │
│      │   │ which should   │   │
│      │   │ wrap at the    │   │
│      │   │ appropriate    │   │
│      │   │ width, which   │   │
│      │   │ will also chop │   │
│      │   │ down reallyre- │   │
│      │   │ allyreallyrea- │   │
│      │   │ llyreallylong  │   │
│      │   │ words if       │   │
│      │   │ needed         │   │
├──────┼───┼────────────────┼───┤
│ row2 │ a │ b              │ c │
└──────┴───┴────────────────┴───┘
krow {
    includeLeadingColumn = false
    header {
        row {
            column("column 1")
            column("column 2") { width = 16 }
            column("column 3")
        }
    }
    bodyRow("row1") { cells("1", "2", "3") }
    bodyRow("row2") { cells("a", "b", "c") }

    cellAt("row1", "column 2") {
        content = "This is some long string which should wrap at the " +
            "appropriate width, which will also chop down reallyreallyreallyreallyreallylong " +
            "words if needed"
    }
}
┌──────────┬────────────────┬──────────┐
│ column 1 │ column 2       │ column 3 │
├──────────┼────────────────┼──────────┤
│ 1        │ This is some   │ 3        │
│          │ long string    │          │
│          │ which should   │          │
│          │ wrap at the    │          │
│          │ appropriate    │          │
│          │ width, which   │          │
│          │ will also chop │          │
│          │ down reallyre- │          │
│          │ allyreallyrea- │          │
│          │ llyreallylong  │          │
│          │ words if       │          │
│          │ needed         │          │
├──────────┼────────────────┼──────────┤
│ a        │ b              │ c        │
└──────────┴────────────────┴──────────┘

ASCII tables can be customized with several border styles. Currently, you can only apply a single border style to the entire table, but in the future I plan on adding support for styling the header row or leading column differently, or even applying custom border styles to individual cells.

+------+----------+----------+----------+
|      | column 1 | column 2 | column 3 |
+------+----------+----------+----------+
| row1 | 1        | 2                   |
+------+----------+                     |
| row2 | a        |                     |
+------+----------+---------------------+
╔══════╦══════════╦══════════╦══════════╗
║      ║ column 1 ║ column 2 ║ column 3 ║
╠══════╬══════════╬══════════╩══════════╣
║ row1 ║ 1        ║ 2                   ║
╠══════╬══════════╣                     ║
║ row2 ║ a        ║                     ║
╚══════╩══════════╩═════════════════════╝
╭╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌╌╌╮
╎      ╎ column 1 ╎ column 2 ╎ column 3 ╎
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌┤
╎ row1 ╎ 1        ╎ 2                   ╎
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤                     ╎
╎ row2 ╎ a        ╎                     ╎
╰╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌┴╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
┌──────┬──────────┬──────────┬──────────┐
│      │ column 1 │ column 2 │ column 3 │
├──────┼──────────┼──────────┴──────────┤
│ row1 │ 1        │ 2                   │
├──────┼──────────┤                     │
│ row2 │ a        │                     │
└──────┴──────────┴─────────────────────┘
┏━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃      ┃ column 1 ┃ column 2 ┃ column 3 ┃
┣━━━━━━╋━━━━━━━━━━╋━━━━━━━━━━┻━━━━━━━━━━┫
┃ row1 ┃ 1        ┃ 2                   ┃
┣━━━━━━╋━━━━━━━━━━┫                     ┃
┃ row2 ┃ a        ┃                     ┃
┗━━━━━━┻━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━┛

HTML

Tables are rendered to ASCII with the HtmlTableFormatter.

val table = krow {
    // ...
}
HtmlTableFormatter().print(table)

Example output:

<table>
  <thead>
  <tr>
    <th></th>
    <th>column 1</th>
    <th>column 2</th>
    <th>column 3</th>
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>row1</td>
    <td>1</td>
    <td rowspan="2" colspan="2">2</td>
  </tr>
  <tr>
    <td>row2</td>
    <td>a</td>
  </tr>
  </tbody>
</table>

column 1 column 2 column 3
row1 1 2
row2 a