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.() -> Unitand 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
blockisn'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 yourcreateHtmlPagefunction 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
blockis 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 typeDIVavailable in your code, but you do have an object of typeBODY, which is created by thebodyfunction. So if you change the parameter type ofblockfromDIV.() -> UnittoBODY.() -> Unit, then you can use theBODYobject created bybodyfunction.So you can change the
createHtmlPagefunction to:and then you can provide the object of type
BODYlike 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
bodyfunction accepts an function type with receiver of typeBODY.() -> Unit, the lambda that you pass to thebodyfunction, will be scoped to theBODYclass/type, so that lambda has access to members available in theBODYtype. TheBODYtype normally doesn't have access to theblock()function, but becauseblockis of typeBODY.() -> Unit, instances ofBODYalso have access to theblockfunction as ifblockis 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.