Search code examples
kotlinktorkotlinxkotlinx-html

How do I provide a page template in kotlinx-html?


I would like to generate a bunch of HTML files with kotlinx-html and I want to start each file with the same template. I would like to have a function for the base structure and provide a lamda to this function for the specific content like so (non working code):

// provide block as a div for the sub content, does not work!
private fun createHtmlPage(block : () -> DIV.()): String {
    val html = createHTMLDocument().html {
        head {
            meta { charset = "utf-8" }
            meta { name="viewport"; content="width=device-width, initial-scale=1" }
            title { +"Tables" }
            link(href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css", "style")
        }
        body {
            block {}
            script("", "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js") {}
        }
    }
    return html.serialize(true)
}

and use this function like this (again non working code):

private fun createIndexPage(tables: Tables) {
    val indexFile = File(path, "index.html")

    // call my template function with a lamda - does not work
    val html = createHtmlPage {
        h1 { +"Tables" }
        tables.tableNames.forEach { tableName ->
            a("${tableName}.html") {
                +tableName
            }
            br
        }
    }
    indexFile.writeText(html)
}

Can anyone point me in the direction how to do this?

Additional question

I have found out that project Ktor HTML DSL exists and they have template support on top of kotlinx-html. Am I supposed to use this library instead of kotlinx-html directly? Is it possible to use it without Ktor?


Solution

  • You don't need Ktor. It can be done with just kotlinx-html and plain Kotlin.

    TLDR

    Change block : () -> DIV.() to block : HtmlBlockTag.() -> Unit and change block {} to block(), so that the final code becomes:

    private fun createHtmlPage(block: HtmlBlockTag.() -> Unit): String {
        val html = createHTMLDocument().html {
            head {
                meta { charset = "utf-8" }
                meta { name="viewport"; content="width=device-width, initial-scale=1" }
                title { +"Tables" }
                link(href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css", "style")
            }
            body {
                block()
                script("", "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js") {}
            }
        }
        return html.serialize(true)
    }
    

    So that you can use this function with code like this:

    val result = createHtmlPage {
        div {
            h1 {
                +"It is working!"
            }
        }
    }
    
    println(result)
    

    Then the output will be:

    <!DOCTYPE html>
    <html>
      <head>
        <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta charset="utf-8">
        <meta content="width=device-width, initial-scale=1" name="viewport">
        <title>Tables</title>
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="style">
      </head>
      <body>
        <div>
          <h1>It is working!</h1>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" type=""></script>
      </body>
    </html>
    

    What's wrong with the code

    Your first block of code has this signature:

    private fun createHtmlPage(block : () -> DIV.()): String
    

    This isn't valid Kotlin code, because the type of parameter block isn't valid. Instead of using type () -> DIV.() it should be DIV.() -> Unit. This is a special construct in Kotlin, called a function type with receiver, which allows you to call your createHtmlPage function with a lambda, in which the lambda is scoped to a receiver object of type DIV.

    So the function should be changed to:

    private fun createHtmlPage(block: DIV.() -> Unit): String
    

    The second part that is not valid is this:

    body {
        block {}
    }
    

    Because the parameter called block is of type DIV.() -> Unit, it needs to have access to an argument of type DIV. You don't have to pass with argument like in a normal function call, like block(someDiv), but it still needs access to it. But you don't have an object of type DIV available in your code, but you do have an object of type BODY, which is created by the body function. So if you change the parameter type of block from DIV.() -> Unit to BODY.() -> Unit, then you can use the BODY object created by body function.

    So you can change the createHtmlPage function to:

    private fun createHtmlPage(block: BODY.() -> Unit): String
    

    and then you can provide the object of type BODY like this to block:

    body {
       [email protected](this@body)
    }
    

    which can be shortened to:

    body {
       this.block(this)
    }
    

    which can be shortened to:

    body {
       block()
    }
    

    This last shortening step may be difficult to understand, but it's like this: because body function accepts an function type with receiver of type BODY.() -> Unit, the lambda that you pass to the body function, will be scoped to the BODY class/type, so that lambda has access to members available in the BODY type. The BODY type normally doesn't have access to the block() function, but because block is of type BODY.() -> Unit, instances of BODY also have access to the block function as if block is a member of BODY. Because the call to block() is scoped to an instance of BODY, calling block() behaves like calling aBodyInstance.block(), which is the same as block(aBodyInstance). Because of this, at this location in the code, you can also call is with just block().

    Instead of using createHtmlPage(block: BODY.() -> Unit) you could also use createHtmlPage(block: HtmlBlockTag.() -> Unit) so that you can use it in other places.