Repository: ajalt/mordant (kotlin)

Issue:
Tables (and therefore almost all layouts) cannot be empty
This complicates items which are rendered by loops.

Consider
```kotlin
fun thing(strings: List<String>) = verticalLayout {
  for (string in strings) {
    cell("Hi $string!")
  }
}
```

This crashes, and so you have to do something like:
```kotlin
fun thing(strings: List<String>) = if (strings.isEmpty()) {
  Text("")
} else {
  verticalLayout {
    for (string in strings) {
      cell("Hi $string!")
    }
  }
}
```

This occurs for `table`, `grid`, and `verticalLayout`. Weirdly, `horizontalLayout` does not exhibit this behavior (presumably because it always adds a single row which is empty?).

Is there a technical reason to prevent empty layouts?

```kotlin
fun main() {
  val t = Terminal()
  println("Vertical")
  runCatching { t.println(verticalLayout { }) }.exceptionOrNull()?.printStackTrace(System.out)
  println("Horizontal")
  runCatching { t.println(horizontalLayout { }) }.exceptionOrNull()?.printStackTrace(System.out)
  println("Grid")
  runCatching { t.println(grid { }) }.exceptionOrNull()?.printStackTrace(System.out)
  println("Table")
  runCatching { t.println(table { }) }.exceptionOrNull()?.printStackTrace(System.out)
}
```
```
Vertical
java.lang.IllegalArgumentException: Table cannot be empty
	at com.github.ajalt.mordant.table.TableImpl.<init>(Table.kt:80)
	at com.github.ajalt.mordant.table.TableLayout.buildTable(TableLayout.kt:24)
	at com.github.ajalt.mordant.table.VerticalLayoutBuilderInstance.build(TableDslInstances.kt:223)
	at com.github.ajalt.mordant.table.TableDslKt.verticalLayout(TableDsl.kt:298)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt:12)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt)
Horizontal

Grid
java.lang.IllegalArgumentException: Table cannot be empty
	at com.github.ajalt.mordant.table.TableImpl.<init>(Table.kt:80)
	at com.github.ajalt.mordant.table.TableLayout.buildTable(TableLayout.kt:24)
	at com.github.ajalt.mordant.table.GridBuilderInstance.build(TableDslInstances.kt:149)
	at com.github.ajalt.mordant.table.TableDslKt.grid(TableDsl.kt:277)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt:16)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt)
Table
java.lang.IllegalArgumentException: Table cannot be empty
	at com.github.ajalt.mordant.table.TableImpl.<init>(Table.kt:80)
	at com.github.ajalt.mordant.table.TableLayout.buildTable(TableLayout.kt:24)
	at com.github.ajalt.mordant.table.TableDslKt.table(TableDsl.kt:254)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt:18)
	at com.jakewharton.mosaic.MordantKt.main(mordant.kt)
```

I am using 2.0.0-beta9.

Pull request description:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c06abcdd9..4d8f6c455 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,14 @@
 # Changelog
 
 ## Unreleased
+### Added
+- Tables and other layouts can now be completely empty [(#82)](https://github.com/ajalt/mordant/issues/82)
+
 ### Changed
 - JVM: terminal detection now uses JNA to call kernel functions directly. 
 - Interactive state of stdin and stdout are now detected separately. 
 - Terminal size detection is now fast since it does not need a subprocess.
+- Update Kotlin to 1.8.0
 
 ### Fixed
 - Detect terminal correctly when running in the IntelliJ terminal tab, but not through a run action. [(#76)](https://github.com/ajalt/mordant/issues/76)
@@ -12,6 +16,9 @@
 ### Deprecated 
 - `timeoutMs` parameter to `TerminalDetection.updateTerminalSize`. This function is now fast on all platforms. 
 
+### Removed
+- Removed JS/Legacy publication. The JS target only publishes artifact build with the IR compiler.
+
 ## 2.0.0-beta9
 ### Changed
 - Stop stripping trailing newline from text when using `Whitespace.PRE` [(#75)](https://github.com/ajalt/mordant/issues/75)
diff --git a/mordant/build.gradle.kts b/mordant/build.gradle.kts
index fb3c71c4d..f41383089 100644
--- a/mordant/build.gradle.kts
+++ b/mordant/build.gradle.kts
@@ -21,7 +21,7 @@ buildscript {
 
 kotlin {
     jvm()
-    js(BOTH) {
+    js(IR) {
         nodejs()
         browser()
     }
@@ -32,13 +32,6 @@ kotlin {
     mingwX64()
 
     sourceSets {
-        all {
-            with(languageSettings) {
-                languageVersion = "1.6"
-                apiVersion = "1.6"
-                optIn("kotlin.RequiresOptIn")
-            }
-        }
         val gen by creating { }
         val commonMain by getting {
             dependsOn(gen)
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
index 8c0827489..e46cd8d11 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
@@ -31,20 +31,28 @@ interface CellStyleBuilderBase {
 }
 
 interface CellStyleBuilder : CellStyleBuilderBase {
+    /**
+     * Borders around each individual cell.
+     *
+     * For cells on the edges of the table, the outside borders can be overridden by
+     * [tableBorders][TableBuilder.tableBorders]
+     */
     var cellBorders: Borders?
+
+    /** Vertical alignment of cell contents */
     var verticalAlign: VerticalAlign?
 
     /**
      * The padding around each cell
      *
-     * If you want to set the padding on around the entire widget, use [withPadding].
+     * If you want to set the padding around the entire layout, use [withPadding].
      */
     var padding: Padding?
 
     /**
      * Set the padding around each cell
      *
-     * If you want to set the padding around the entire widget, use [withPadding].
+     * If you want to set the padding around the entire layout, use [withPadding].
      */
     fun padding(all: Int) {
         padding = Padding(all)
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
index 456d3067a..d4abc4384 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
@@ -212,6 +212,7 @@ internal class VerticalLayoutBuilderInstance(
     fun build(): Widget {
         tableBuilder.padding = Padding(0)
         tableBuilder.cellBorders = Borders.NONE
+        tableBuilder.tableBorders = Borders.NONE
         tableBuilder.column(0) { this@column.width = this@VerticalLayoutBuilderInstance.width }
         if (spacing > 0) {
             for ((i, row) in section.rows.withIndex()) {
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableLayout.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableLayout.kt
index ac3a47a62..f1b420c93 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableLayout.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableLayout.kt
@@ -1,6 +1,7 @@
 package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.rendering.*
+import com.github.ajalt.mordant.widgets.EmptyWidget
 import com.github.ajalt.mordant.widgets.Padding
 import com.github.ajalt.mordant.widgets.Text
 import com.github.ajalt.mordant.widgets.withPadding
@@ -8,6 +9,7 @@ import com.github.ajalt.mordant.widgets.withPadding
 internal typealias ImmutableRow = List<Cell>
 internal typealias MutableRow = MutableList<Cell>
 
+
 /**
  * Takes a table builder and computes the locations, spans, and styles of all [Cell]s.
  * Returns a [TableImpl] that will compute the row and column sizes and draw the table.
@@ -20,9 +22,15 @@ internal class TableLayout(private val table: TableBuilderInstance) {
         val header = buildSection(table.headerSection, builderWidth)
         val body = buildSection(table.bodySection, builderWidth)
         val footer = buildSection(table.footerSection, builderWidth)
-
+        val rows = listOf(header, body, footer).flatten().ifEmpty {
+            val b = table.tableBorders ?: Borders.ALL
+            val cell = Cell.Content(
+                EmptyWidget, 1, 1, b.left, b.top, b.right, b.bottom, null, TextAlign.LEFT, VerticalAlign.TOP
+            )
+            listOf(listOf(cell))
+        }
         return TableImpl(
-            rows = listOf(header, body, footer).flatten(),
+            rows = rows,
             borderType = table.borderType,
             borderStyle = table.borderStyle,
             headerRowCount = header.size,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b0476de59..4c0a3c1b8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,19 +9,19 @@ include("samples:table")
 dependencyResolutionManagement {
     versionCatalogs {
         create("libs") {
-            version("kotlin", "1.7.21")
+            version("kotlin", "1.8.0")
             version("jna", "5.12.1")
 
-            plugin("dokka", "org.jetbrains.dokka").version("1.7.10")
-            library("dokka-base", "org.jetbrains.dokka:dokka-base:1.7.10")
+            plugin("dokka", "org.jetbrains.dokka").version("1.7.20")
+            library("dokka-base", "org.jetbrains.dokka:dokka-base:1.7.20")
 
             library("colormath", "com.github.ajalt.colormath:colormath:3.2.1")
-            library("markdown", "org.jetbrains:markdown:0.3.1")
+            library("markdown", "org.jetbrains:markdown:0.3.6")
 
             library("jna-core", "net.java.dev.jna", "jna").versionRef("jna")
 
             // used in tests
-            library("kotest", "io.kotest:kotest-assertions-core:5.2.1")
+            library("kotest", "io.kotest:kotest-assertions-core:5.5.4")
             library("systemrules", "com.github.stefanbirkner:system-rules:1.19.0")
         }
     }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 774cc7a07..bdec8e233 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,7 +33,7 @@ jobs:
           java-version: 17
       - uses: gradle/gradle-build-action@v2
         with:
-          arguments: :mordant:jvmTest :mordant:jsIrTest :mordant:jsLegacyTest ${{matrix.NATIVE_TEST_TASK}} --stacktrace
+          arguments: :mordant:jvmTest :mordant:jsTest ${{matrix.NATIVE_TEST_TASK}} --stacktrace
       - name: Bundle the build report
         if: failure()
         run: find . -type d -name 'reports' | zip -@ -r build-reports.zip
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/LinearLayoutTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/LinearLayoutTest.kt
index 0b6fcc710..f1782d6e7 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/LinearLayoutTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/LinearLayoutTest.kt
@@ -16,6 +16,10 @@ import kotlin.js.JsName
 import kotlin.test.Test
 
 class LinearLayoutTest : RenderingTest() {
+    @Test
+    fun horizontalLayoutEmpty() = checkRender(
+        horizontalLayout {}, "", width = 10
+    )
     @Test
     fun horizontalLayout() = checkRender(
         horizontalLayout {
@@ -39,6 +43,12 @@ class LinearLayoutTest : RenderingTest() {
         """, width = 10
     )
 
+    @Test
+    fun verticalLayoutEmpty() = checkRender(
+        verticalLayout {}, """
+        ░
+        """
+    )
     @Test
     fun verticalLayout() = checkRender(
         verticalLayout {
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
index 9aed0eebd..01cb9089e 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
@@ -16,6 +16,16 @@ import kotlin.js.JsName
 import kotlin.test.Test
 
 class TableTest : RenderingTest() {
+    @Test
+    @JsName("empty_table")
+    fun `empty table`() = doBodyTest(
+        """
+    ░┌┐
+    ░││
+    ░└┘
+    """
+    ) {}
+
     @Test
     @JsName("empty_cell")
     fun `empty cell`() = doBodyTest(
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalColorsTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalColorsTest.kt
index be12d3f0b..bc49cae0b 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalColorsTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalColorsTest.kt
@@ -93,20 +93,21 @@ class TerminalColorsTest {
         color.color!!.toAnsi16().code shouldBe code
     }
 
-    @Test
-    @JsName("all_24bit_colors")
-    fun `all 24bit colors`() = forAll(
-        row(c.rgb("#ff00ff"), RGB("#ff00ff")),
-        row(c.rgb(.11, .22, .33), RGB(.11, .22, .33)),
-        row(c.hsl(.11, .22, .33), HSL(.11, .22, .33)),
-        row(c.hsv(11, .22, .33), HSV(11, .22, .33)),
-        row(c.cmyk(11, 22, 33, 44), CMYK(11, 22, 33, 44)),
-        row(c.gray(0.5), RGB(.5, .5, .5)),
-        row(c.xyz(.11, .22, .33), XYZ(.11, .22, .33)),
-        row(c.lab(11, 22, 33), LAB(11, 22, 33)),
-    ) { color, expected ->
-        color.color shouldBe expected
-    }
+    // Disabled due to codegen bug on JS/IR
+//    @Test
+//    @JsName("all_24bit_colors")
+//    fun `all 24bit colors`() = forAll(
+//        row(c.rgb("#ff00ff"), RGB("#ff00ff")),
+//        row(c.rgb(.11, .22, .33), RGB(.11, .22, .33)),
+//        row(c.hsl(.11, .22, .33), HSL(.11, .22, .33)),
+//        row(c.hsv(11, .22, .33), HSV(11, .22, .33)),
+//        row(c.cmyk(11, 22, 33, 44), CMYK(11, 22, 33, 44)),
+//        row(c.gray(0.5), RGB(.5, .5, .5)),
+//        row(c.xyz(.11, .22, .33), XYZ(.11, .22, .33)),
+//        row(c.lab(11, 22, 33), LAB(11, 22, 33)),
+//    ) { color, expected ->
+//        color.color shouldBe expected
+//    }
 
     @Test
     @JsName("all_styles")