
Type-safe, context-aware DSL for building SVG markup and DOM elements, with animation support, pretty-printing, and unsafe/custom-element APIs for manual attributes and advanced hacks.
This library is an implementation of the current second edition of the SVG 1.1 spec as a Kotlin DSL (or type safe builder). This library strives for maximal type safety and the avoidance of string typing as much as possible. This library makes use of the currently experimental multiple context parameters feature in Kotlin. The library's design (and some of the implementation) is based on kotlinx-html. Like kotlinx-html, this library is fully multiplatform and can generate DOM elements on the js and wasmJs targets.
This library is essentially a work in progress that I have decided to do in the public view. That means that in the
current state, this library is not up to my standards for "open source." I encourage you to try it out if you found this
page but be aware that there may be bugs. See the section below on the unsafe API for says to get around any bugs you
face. I encourage feedback and plan to continue developing this into a high quality open source library.
An alpha version of this library is published on Maven Central. Coordinates:
groupId: dev.jamesyox
artifactId: svg4k
version: 0.1.0-alpha.1
If you use Gradle you should be able to add the following to your dependencies to use svg4k:
implementation("dev.jamesyox:svg4k:0.1.0-alpha.1")You are now free to use this in a project, however you must enable context parameters. This library is based around that feature.
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}
// ....
implementation("dev.jamesyox:svg4k:0.1.0-alpha.1")Please note that this library is an alpha and is based around a currently experimental Kotlin language feature. Therefore, you should use caution before using this for anything too serious.
If you are familiar with the kotlinx-html then the syntax should feel pretty familiar:
val svgStr = svgString(isPrettyPrint = true) {
svg {
viewBox = ViewBox(0, 0, 200, 100)
path {
fill = "none"
stroke = "lightgrey"
d {
M(20, 50)
C(20, -50, 180, 150, 180, 50)
C(180, -50, 20, 150, 20, 50)
Z
}
}
circle {
r = 5.none
fill = "red"
animateMotion {
dur = Dur.ClockValue(10.seconds)
repeatCount = RepeatCount.Indefinite
path {
M(20, 50)
C(20, -50, 180, 150, 180, 50)
C(180, -50, 20, 150, 20, 50)
Z
}
}
}
}
}
println(svgString)Would output
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 100">
<path
fill="none"
stroke="lightgrey"
d="M 20 50 C 20 -50 180 150 180 50 C 180 -50 20 150 20 50 Z" />
<circle
r="5"
fill="red">
<animateMotion
dur="10s"
repeatCount="indefinite"
path="M 20 50 C 20 -50 180 150 180 50 C 180 -50 20 150 20 50 Z" />
</circle>
</svg>
Which if rendered by a browser would look like:
Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/animateMotion
I eventually plan to add more docs but a good overview of the syntax can be found in the tests I have written so far.
https://github.com/yoxjames/svg4k/tree/main/src/commonTest/kotlin/dev/jamesyox/svg4k/tags
Also like kotlinx-html this builder can be used on any Appendable. This is done with the appendSVG { ... }
extension fun.
On JVM this could be used to create a file like:
FileWriter("image.svg").use { it.appendSVG { svg { /* ... */ } } }Like kotlinx-html this project can be used on Js and WasmJS to generate DOM elements instead of just text. For
example:
// Available only in JS and WasmJS source sets
val rect: SVGElement // Allowed since this uses the `callsInPlace` Contract. You can use this to add listeners, etc.
val domElement: SVGElement = createSvg {
svg {
rect = rect {
// ...
}
}
}
domElement.onclick = { /* ... */ } // This is a DOM element so you can use browser DOM methods
rect.onclick = { /* ... */ } // Same here.As of today (though I hope to get a PR into kotlinx-html that will make this better), there's not a great way to interop
with kotlinx-html. Ideally I want to do something like this:
html { // kotlinx-html
body { // kotlin-html
svg { // svg4k >>> THIS DOES NOT CURRENTLY WORK
// ... // svg4k
}
}
}With context parameters this should be possible. On most targets it pretty much is. You could easily write an extension like this:
private fun HTMLTag.svg4k(
block: context(AttributeConsumer, @SvgTagDSL Svg) () -> Unit
) {
unsafe {
raw(svgString { dev.jamesyox.svg4k.tags.svg { block() } })
}
}This function bridges kotlinx-html to svg4k by basically tapping into the unsafe raw text api
from kotlinx-html. This works for many use cases but if you attempt this same approach on a TagConsumer<T> that uses
the DOM, this will not work. Appending text will not generate dom elements. For that you'll need something much more
ugly like this:
fun <T> TagConsumer<HTMLElement>.svgMagick(
block: context(dev.jamesyox.svg4k.TagConsumer<SVGElement>, RootContainer) () -> T
): T {
val tagConsumer = JsDomTagConsumer(document)
val output = block(tagConsumer, RootContainer)
val svgDom = tagConsumer.output()
val hackDiv = div { } // We simply need to create a DOM element to access the parent (wasteful)
val currentNode = hackDiv.parentNode
currentNode?.removeChild(hackDiv)
currentNode?.let {
svgDom.forEach { child -> it.appendChild(child) }
}
return output
}I'm hoping to open a PR with kotlinx-html soon that should make seamless interop possible.
I'll update this README once that's opened!
This library may have bugs. A lot of those bugs may be around typing and scoping. Lets say for some reason cx was
now allowed on Circle elements. You could force the issue like this:
circle {
unsafe { // Any attribute is allowed here
cx = 5.none
}
}You can also create invalid svg element hierarchies. Like normally you cant have a circle inside of a circle but
with the Unsafe API you can do anything you want, no matter how unwise! Generally this is intended to work around bugs
by providing a version of the API with some type checking off.
circle {
unsafe {
circle { /* ... */ }
}
}Would yield the invalid <circle><circle></circle></circle> result.
unsafe { ... } also allows you to add arbitrary references. For instance:
val expected = """
<svg
xmlns="http://www.w3.org/2000/svg">
<circle
not-real="madeup" />
</svg>
""".trimIndent()
val actual = svgString(isPrettyPrint = true) {
svg {
circle {
unsafe {
attr["not-real"] = "madeup"
}
}
}
}
assertEquals(expected, actual)In this case we used attr which is available inside the unsafe scope to add an attribute called not-real and set
it to madeup. You can use this to set attributes that exist to arbitrary values as well.
You can also use unsafe { ... } to construct arbitrary elements via customElement(...):
val expected = """
<svg
xmlns="http://www.w3.org/2000/svg">
<fake
fr="5">
<circle />
</fake>
</svg>
""".trimIndent()
val actual = svgString(isPrettyPrint = true) {
svg {
unsafe {
// Create element <fake>
customElement("fake") {
// Any attribute can be used in custom elements
fr = 5.none
// Any elements can be added to custom elements
circle {
}
}
}
}
}Unsafe is meant to work around issues you may find in the library or perhaps to do things I had not considered. However,
it does have some limitations. One big one is that unsafe { ... } only makes things unsafe inside the lambda provided.
For instance, you cannot do something like:
circle {
unsafe {
// unsafe applies at this level!
rect { // This compiles but is not a valid SVG!
// unsafe does not apply at this level!
fr = 5.none // `fr` does not apply to rect so this wont compile!
}
}
}This won't be allowed even with unsafe because unsafe { ... } is marked with the SvgTagDsl marker which is marked
with DskMarker.
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-dsl-marker/
If you want to truly turn off safety you'll need the unsafeCascading { ... } function which starts an unsafe scope
with no DSL marker letting you go wild:
circle {
unsafeCascading {
// unsafeCascading applies at this level!
rect { // This compiles but is not a valid SVG!
// unsafe still applies at this level too!
fr = 5.none // This compiles but is not a valid SVG!
}
}
}Contributions are welcome. If you find a bug and want to fix it, feel free to do so and open a PR. Additionally, if you are looking for places to contribute, simply search for TODO and you'll see every area I have identified as lacking and want to address prior to release. There's a lot of work still to be done!
A lot of this project was inspired by kotlinx-html by JetBrains, the overall design was reused but reimagined using multiple context parameters. A lot of the tag consumption logic was based on the tag consumption in kotlinx-html
The documentation and visibility of each element/attribution on specific parents was largely based on Mozilla's SVG documentation. I highly recommend this as a resource to anyone wanting to learn more about SVG. A lot of my test cases come from examples in this document.
Last but not least is the actual SVG 1.1 spec which is the actual protocol this library models.
This library is an implementation of the current second edition of the SVG 1.1 spec as a Kotlin DSL (or type safe builder). This library strives for maximal type safety and the avoidance of string typing as much as possible. This library makes use of the currently experimental multiple context parameters feature in Kotlin. The library's design (and some of the implementation) is based on kotlinx-html. Like kotlinx-html, this library is fully multiplatform and can generate DOM elements on the js and wasmJs targets.
This library is essentially a work in progress that I have decided to do in the public view. That means that in the
current state, this library is not up to my standards for "open source." I encourage you to try it out if you found this
page but be aware that there may be bugs. See the section below on the unsafe API for says to get around any bugs you
face. I encourage feedback and plan to continue developing this into a high quality open source library.
An alpha version of this library is published on Maven Central. Coordinates:
groupId: dev.jamesyox
artifactId: svg4k
version: 0.1.0-alpha.1
If you use Gradle you should be able to add the following to your dependencies to use svg4k:
implementation("dev.jamesyox:svg4k:0.1.0-alpha.1")You are now free to use this in a project, however you must enable context parameters. This library is based around that feature.
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}
// ....
implementation("dev.jamesyox:svg4k:0.1.0-alpha.1")Please note that this library is an alpha and is based around a currently experimental Kotlin language feature. Therefore, you should use caution before using this for anything too serious.
If you are familiar with the kotlinx-html then the syntax should feel pretty familiar:
val svgStr = svgString(isPrettyPrint = true) {
svg {
viewBox = ViewBox(0, 0, 200, 100)
path {
fill = "none"
stroke = "lightgrey"
d {
M(20, 50)
C(20, -50, 180, 150, 180, 50)
C(180, -50, 20, 150, 20, 50)
Z
}
}
circle {
r = 5.none
fill = "red"
animateMotion {
dur = Dur.ClockValue(10.seconds)
repeatCount = RepeatCount.Indefinite
path {
M(20, 50)
C(20, -50, 180, 150, 180, 50)
C(180, -50, 20, 150, 20, 50)
Z
}
}
}
}
}
println(svgString)Would output
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 100">
<path
fill="none"
stroke="lightgrey"
d="M 20 50 C 20 -50 180 150 180 50 C 180 -50 20 150 20 50 Z" />
<circle
r="5"
fill="red">
<animateMotion
dur="10s"
repeatCount="indefinite"
path="M 20 50 C 20 -50 180 150 180 50 C 180 -50 20 150 20 50 Z" />
</circle>
</svg>
Which if rendered by a browser would look like:
Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/animateMotion
I eventually plan to add more docs but a good overview of the syntax can be found in the tests I have written so far.
https://github.com/yoxjames/svg4k/tree/main/src/commonTest/kotlin/dev/jamesyox/svg4k/tags
Also like kotlinx-html this builder can be used on any Appendable. This is done with the appendSVG { ... }
extension fun.
On JVM this could be used to create a file like:
FileWriter("image.svg").use { it.appendSVG { svg { /* ... */ } } }Like kotlinx-html this project can be used on Js and WasmJS to generate DOM elements instead of just text. For
example:
// Available only in JS and WasmJS source sets
val rect: SVGElement // Allowed since this uses the `callsInPlace` Contract. You can use this to add listeners, etc.
val domElement: SVGElement = createSvg {
svg {
rect = rect {
// ...
}
}
}
domElement.onclick = { /* ... */ } // This is a DOM element so you can use browser DOM methods
rect.onclick = { /* ... */ } // Same here.As of today (though I hope to get a PR into kotlinx-html that will make this better), there's not a great way to interop
with kotlinx-html. Ideally I want to do something like this:
html { // kotlinx-html
body { // kotlin-html
svg { // svg4k >>> THIS DOES NOT CURRENTLY WORK
// ... // svg4k
}
}
}With context parameters this should be possible. On most targets it pretty much is. You could easily write an extension like this:
private fun HTMLTag.svg4k(
block: context(AttributeConsumer, @SvgTagDSL Svg) () -> Unit
) {
unsafe {
raw(svgString { dev.jamesyox.svg4k.tags.svg { block() } })
}
}This function bridges kotlinx-html to svg4k by basically tapping into the unsafe raw text api
from kotlinx-html. This works for many use cases but if you attempt this same approach on a TagConsumer<T> that uses
the DOM, this will not work. Appending text will not generate dom elements. For that you'll need something much more
ugly like this:
fun <T> TagConsumer<HTMLElement>.svgMagick(
block: context(dev.jamesyox.svg4k.TagConsumer<SVGElement>, RootContainer) () -> T
): T {
val tagConsumer = JsDomTagConsumer(document)
val output = block(tagConsumer, RootContainer)
val svgDom = tagConsumer.output()
val hackDiv = div { } // We simply need to create a DOM element to access the parent (wasteful)
val currentNode = hackDiv.parentNode
currentNode?.removeChild(hackDiv)
currentNode?.let {
svgDom.forEach { child -> it.appendChild(child) }
}
return output
}I'm hoping to open a PR with kotlinx-html soon that should make seamless interop possible.
I'll update this README once that's opened!
This library may have bugs. A lot of those bugs may be around typing and scoping. Lets say for some reason cx was
now allowed on Circle elements. You could force the issue like this:
circle {
unsafe { // Any attribute is allowed here
cx = 5.none
}
}You can also create invalid svg element hierarchies. Like normally you cant have a circle inside of a circle but
with the Unsafe API you can do anything you want, no matter how unwise! Generally this is intended to work around bugs
by providing a version of the API with some type checking off.
circle {
unsafe {
circle { /* ... */ }
}
}Would yield the invalid <circle><circle></circle></circle> result.
unsafe { ... } also allows you to add arbitrary references. For instance:
val expected = """
<svg
xmlns="http://www.w3.org/2000/svg">
<circle
not-real="madeup" />
</svg>
""".trimIndent()
val actual = svgString(isPrettyPrint = true) {
svg {
circle {
unsafe {
attr["not-real"] = "madeup"
}
}
}
}
assertEquals(expected, actual)In this case we used attr which is available inside the unsafe scope to add an attribute called not-real and set
it to madeup. You can use this to set attributes that exist to arbitrary values as well.
You can also use unsafe { ... } to construct arbitrary elements via customElement(...):
val expected = """
<svg
xmlns="http://www.w3.org/2000/svg">
<fake
fr="5">
<circle />
</fake>
</svg>
""".trimIndent()
val actual = svgString(isPrettyPrint = true) {
svg {
unsafe {
// Create element <fake>
customElement("fake") {
// Any attribute can be used in custom elements
fr = 5.none
// Any elements can be added to custom elements
circle {
}
}
}
}
}Unsafe is meant to work around issues you may find in the library or perhaps to do things I had not considered. However,
it does have some limitations. One big one is that unsafe { ... } only makes things unsafe inside the lambda provided.
For instance, you cannot do something like:
circle {
unsafe {
// unsafe applies at this level!
rect { // This compiles but is not a valid SVG!
// unsafe does not apply at this level!
fr = 5.none // `fr` does not apply to rect so this wont compile!
}
}
}This won't be allowed even with unsafe because unsafe { ... } is marked with the SvgTagDsl marker which is marked
with DskMarker.
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-dsl-marker/
If you want to truly turn off safety you'll need the unsafeCascading { ... } function which starts an unsafe scope
with no DSL marker letting you go wild:
circle {
unsafeCascading {
// unsafeCascading applies at this level!
rect { // This compiles but is not a valid SVG!
// unsafe still applies at this level too!
fr = 5.none // This compiles but is not a valid SVG!
}
}
}Contributions are welcome. If you find a bug and want to fix it, feel free to do so and open a PR. Additionally, if you are looking for places to contribute, simply search for TODO and you'll see every area I have identified as lacking and want to address prior to release. There's a lot of work still to be done!
A lot of this project was inspired by kotlinx-html by JetBrains, the overall design was reused but reimagined using multiple context parameters. A lot of the tag consumption logic was based on the tag consumption in kotlinx-html
The documentation and visibility of each element/attribution on specific parents was largely based on Mozilla's SVG documentation. I highly recommend this as a resource to anyone wanting to learn more about SVG. A lot of my test cases come from examples in this document.
Last but not least is the actual SVG 1.1 spec which is the actual protocol this library models.