added a few experimental alternatives implementations of string templates for string/html/jdbc etc for KT-1565 seems there's a few ways we could do it...
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
== Warning experimental APIs
|
||||
|
||||
This package contains a couple of alternative experimental implementations of string templates.
|
||||
|
||||
These are not yet integrated into the language, but are here to compare implementation details,
|
||||
suitability, code size, complexity and efficiency.
|
||||
|
||||
=== Aims
|
||||
|
||||
* make it easy to generate various kinds of output such as text, text with internationalisation, HTML/XML, JSON, URL/URLs or make JDBC calls while reusing the same language templates with $ expressions
|
||||
|
||||
=== Issues
|
||||
|
||||
* how many objects are created per call?
|
||||
* how extensible & easy to reuse the concept of String Templates in other library features
|
||||
* pass context into the template somehow; so that formatting options (Locales, nullText, JDBC Connection, converters or whatnot) can be customized
|
||||
* avoid where possible lots of "if (value is SomeType)" runtime checks to determine what encoding strategy should be used
|
||||
|
||||
|
||||
==== experiment1
|
||||
|
||||
Here we convert a string template into a function on some kind of builder object where we invoke the text() method for static text and expression() for dynamic expressions
|
||||
|
||||
Pro:
|
||||
|
||||
* simple and efficient code is generated - no arrays
|
||||
* no arrays are created for static and expression arguments
|
||||
* static dispatch of encoding functions; no runtime instanceof checks on each argument
|
||||
|
||||
* Cons:
|
||||
|
||||
* function may create an inner object/class (though the compiler may get smart enough to inline some of those - or maybe generate a custom function for the whole thing with parameters?)
|
||||
* we can only use this strategy if the template expression is passed to a function and we can detect the function parameter takes a fn: (T) -> Unit; and T has suitable text() and expression() methods.
|
||||
(We can use extension methods to allow regular builder-like classes such as java.lang.StringBuilder to be used to minimise redundant object creation) - so the code generation is a bit more complex
|
||||
|
||||
==== experiment2
|
||||
|
||||
Here we create a single StringTemplate object containing the constant text array and the values array. Then we use functions or extension functions as ways to do other things.
|
||||
|
||||
Pros:
|
||||
|
||||
* really simple
|
||||
* no real compiler magic required where the type depends on the context
|
||||
|
||||
Cons:
|
||||
|
||||
* requires runtime instanceof checks on each expression value
|
||||
* fair bit of object construction per call (2 arrays, a template object, then the string builder and lots of index lookup of arrays
|
||||
|
||||
==== experiment 3
|
||||
|
||||
The idea is we create a FooTemplate object with the static constant strings inside; so we can easily cache the immutable stuff on startup. Then we basically use pseudo code like
|
||||
|
||||
val template = StringTemplate("some ", " static ", "text")
|
||||
...
|
||||
val builder = template.builder()
|
||||
builder.expression(foo)
|
||||
builder.expression(bar)
|
||||
val answer = builder.build()
|
||||
|
||||
Then based on the context - e.g. an annotation or a method parameter type or something; we determine which kind of template to create (StringTemplate, HtmlTemplate, JdbcTemplate etc)
|
||||
|
||||
Pros:
|
||||
|
||||
* fairly simple
|
||||
|
||||
Cons:
|
||||
|
||||
* quite a few different types (FooTemplate and FooBuilder)
|
||||
* no way to pass in parameters to the FooBuilder class; for example the JDBC connection, or the Locale / NumberFormats to use for numeric formatting etc
|
||||
* requires runtime instanceof checks on each expression value & array access etc
|
||||
@@ -0,0 +1,96 @@
|
||||
package kotlin.template.experiment1
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import kotlin.dom.toXmlString
|
||||
|
||||
fun StringBuilder.text(value : String) : Unit {
|
||||
this.append(value)
|
||||
}
|
||||
|
||||
fun StringBuilder.expression(value : Any?) : Unit {
|
||||
this.append(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a [[StringTemplate]] to a String
|
||||
*/
|
||||
// TODO tried toString() but compiler error kicks in
|
||||
fun format(fn : (StringBuilder) -> Unit) : String {
|
||||
val builder = StringBuilder()
|
||||
fn(builder)
|
||||
return builder.toString() ?: ""
|
||||
}
|
||||
|
||||
|
||||
abstract class TextBuilder<T>(val output: Appendable) {
|
||||
|
||||
/*
|
||||
*/
|
||||
/**
|
||||
* Converts the given string template to a String
|
||||
|
||||
fun toString(fn: (T) -> Unit): String = toString(ToStringFormatter, fn)
|
||||
|
||||
fun toString(formatter : ValueFormatter, fn: (T) -> Unit): String {
|
||||
fn(this)
|
||||
return output.toString() ?: ""
|
||||
}
|
||||
*/
|
||||
|
||||
fun text(value: String): Unit {
|
||||
output.append(value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HtmlBuilder(output: Appendable) : TextBuilder<HtmlBuilder>(output) {
|
||||
var nullText : String = ""
|
||||
|
||||
fun expression(value: Node): Unit {
|
||||
text(value.toXmlString())
|
||||
}
|
||||
|
||||
fun expression(value: Any?): Unit {
|
||||
if (value == null) {
|
||||
output.append(nullText)
|
||||
} else {
|
||||
escape(value.toString() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun escape(text : String) : Unit {
|
||||
for (c in text) {
|
||||
if (c == '<') output.append("<")
|
||||
else if (c == '>') output.append(">")
|
||||
else if (c == '&') output.append("&")
|
||||
else if (c == '"') output.append(""")
|
||||
else output.append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a formatter of values in a [[StringTemplate]] which understands how to escape
|
||||
* different types for different kinds of language
|
||||
*/
|
||||
trait ValueFormatter {
|
||||
fun format(buffer : Appendable, val value : Any?) : Unit
|
||||
}
|
||||
|
||||
object ToStringFormatter : ValueFormatter {
|
||||
|
||||
fun toString() = "ToStringFormatter"
|
||||
|
||||
override fun format(buffer : Appendable, value : Any?) {
|
||||
val text = if (value == null) "null" else value.toString()
|
||||
buffer.append(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun toHtml(fn: (HtmlBuilder) -> Unit): String {
|
||||
val buffer = StringBuilder()
|
||||
val html = HtmlBuilder(buffer)
|
||||
fn(html)
|
||||
return buffer.toString() ?: ""
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package kotlin.template.experiment2
|
||||
|
||||
import kotlin.dom.*
|
||||
import org.w3c.dom.Node
|
||||
|
||||
|
||||
/**
|
||||
* Creates a string template from some constant string expressions and some dynamic expressions.
|
||||
*/
|
||||
class StringTemplate(val constantText : Array<String>, val expressions : Array<Any?>) {
|
||||
|
||||
/**
|
||||
* Converts the given string template to a String
|
||||
*/
|
||||
fun toString(): String = toString(ToStringFormatter)
|
||||
|
||||
fun toString(formatter : ValueFormatter): String {
|
||||
val buffer = StringBuilder()
|
||||
append(buffer, formatter)
|
||||
return buffer.toString() ?: ""
|
||||
}
|
||||
|
||||
fun append(buffer: Appendable, formatter: ValueFormatter): Unit {
|
||||
val expressionSize = expressions.size
|
||||
for (i in 0.upto(constantText.size - 1)) {
|
||||
buffer.append(constantText[i])
|
||||
if (i < expressionSize) {
|
||||
val value = expressions[i]
|
||||
formatter.format(buffer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given template to HTML with an optional formatter
|
||||
*/
|
||||
fun toHtml(formatter: HtmlFormatter = HtmlFormatter()): String = toString(formatter)
|
||||
|
||||
/**
|
||||
* Appends the HTML representation of this template to the given appendable
|
||||
*/
|
||||
fun appendHtml(buffer: Appendable, formatter: HtmlFormatter = HtmlFormatter()): String = toString(formatter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a formatter of values in a [[StringTemplate]] which understands how to escape
|
||||
* different types for different kinds of language
|
||||
*/
|
||||
trait ValueFormatter {
|
||||
fun format(buffer : Appendable, val value : Any?) : Unit
|
||||
}
|
||||
|
||||
object ToStringFormatter : ValueFormatter {
|
||||
|
||||
fun toString() = "ToStringFormatter"
|
||||
|
||||
override fun format(buffer : Appendable, value : Any?) {
|
||||
val text = if (value == null) "null" else value.toString()
|
||||
buffer.append(text)
|
||||
}
|
||||
}
|
||||
|
||||
class HtmlFormatter : ValueFormatter {
|
||||
var nullText : String = ""
|
||||
|
||||
override fun format(buffer : Appendable, value : Any?) {
|
||||
if (value == null) {
|
||||
buffer.append(nullText)
|
||||
} else if (value is StringTemplate) {
|
||||
value.append(buffer, this)
|
||||
} else if (value is Node) {
|
||||
buffer.append(value.toXmlString())
|
||||
} else {
|
||||
escape(buffer, value.toString() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun escape(buffer : Appendable, text : String) : Unit {
|
||||
for (c in text) {
|
||||
if (c == '<') buffer.append("<")
|
||||
else if (c == '>') buffer.append(">")
|
||||
else if (c == '&') buffer.append("&")
|
||||
else if (c == '"') buffer.append(""")
|
||||
else buffer.append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kotlin.template
|
||||
package kotlin.template.experiment3
|
||||
|
||||
import kotlin.dom.toXmlString
|
||||
import org.w3c.dom.Node
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kotlin.template
|
||||
package kotlin.template.experiment3
|
||||
|
||||
/**
|
||||
* Creates a string template from a string with $ expressions inside.
|
||||
Reference in New Issue
Block a user