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
?
You don't need Ktor. It can be done with just kotlinx-html and plain Kotlin.
TLDR
Change
block : () -> DIV.()
toblock : HtmlBlockTag.() -> Unit
and changeblock {}
toblock()
, so that the final code becomes:So that you can use this function with code like this:
Then the output will be:
What's wrong with the code
Your first block of code has this signature:
This isn't valid Kotlin code, because the type of parameter
block
isn't valid. Instead of using type() -> DIV.()
it should beDIV.() -> Unit
. This is a special construct in Kotlin, called a function type with receiver, which allows you to call yourcreateHtmlPage
function with a lambda, in which the lambda is scoped to a receiver object of typeDIV
.So the function should be changed to:
The second part that is not valid is this:
Because the parameter called
block
is of typeDIV.() -> Unit
, it needs to have access to an argument of typeDIV
. You don't have to pass with argument like in a normal function call, likeblock(someDiv)
, but it still needs access to it. But you don't have an object of typeDIV
available in your code, but you do have an object of typeBODY
, which is created by thebody
function. So if you change the parameter type ofblock
fromDIV.() -> Unit
toBODY.() -> Unit
, then you can use theBODY
object created bybody
function.So you can change the
createHtmlPage
function to:and then you can provide the object of type
BODY
like this toblock
:which can be shortened to:
which can be shortened to:
This last shortening step may be difficult to understand, but it's like this: because
body
function accepts an function type with receiver of typeBODY.() -> Unit
, the lambda that you pass to thebody
function, will be scoped to theBODY
class/type, so that lambda has access to members available in theBODY
type. TheBODY
type normally doesn't have access to theblock()
function, but becauseblock
is of typeBODY.() -> Unit
, instances ofBODY
also have access to theblock
function as ifblock
is a member ofBODY
. Because the call toblock()
is scoped to an instance ofBODY
, callingblock()
behaves like callingaBodyInstance.block()
, which is the same asblock(aBodyInstance)
. Because of this, at this location in the code, you can also call is with justblock()
.Instead of using
createHtmlPage(block: BODY.() -> Unit)
you could also usecreateHtmlPage(block: HtmlBlockTag.() -> Unit)
so that you can use it in other places.