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/bootstrap@5.2.0/dist/css/bootstrap.min.css", "style")
}
body {
block {}
script("", "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/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
?
You don't need Ktor. It can be done with just kotlinx-html and plain Kotlin.
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/bootstrap@5.2.0/dist/css/bootstrap.min.css", "style")
}
body {
block()
script("", "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/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/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="style">
</head>
<body>
<div>
<h1>It is working!</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" type=""></script>
</body>
</html>
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 {
this@body.block(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.